You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@paimon.apache.org by ni...@apache.org on 2024/01/10 08:37:55 UTC

(incubator-paimon-webui) branch main updated: [Feature] Enhance metadata options module support (#139)

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 26e1541  [Feature] Enhance metadata options module support (#139)
26e1541 is described below

commit 26e15415686222294827664665e6bf1c481faad8
Author: xiaomo <we...@gmail.com>
AuthorDate: Wed Jan 10 16:37:50 2024 +0800

    [Feature] Enhance metadata options module support (#139)
---
 paimon-web-ui-new/mock/modules/metadata.js         |  29 ++++
 paimon-web-ui-new/package.json                     |   1 -
 paimon-web-ui-new/src/api/models/catalog/index.ts  |  46 +++++-
 .../src/api/models/catalog/types/table.ts          |   6 +-
 .../src/components/dynamic-form/index.tsx          |   6 +-
 .../src/components/dynamic-form/types.ts           |  14 +-
 .../src/components/dynamic-form/use-form.ts        |   7 +-
 paimon-web-ui-new/src/components/modal/index.tsx   |   8 +-
 paimon-web-ui-new/src/form-lib/index.ts            |   2 +
 .../src/form-lib/metadata/use-options.ts           |  65 ++++++++
 paimon-web-ui-new/src/store/catalog/index.ts       |   4 +-
 .../metadata/components/catalog-form/index.tsx     |  27 ++--
 .../metadata/components/database-form/index.tsx    |   7 +-
 .../components/menu-tree/index.module.scss         |  20 ++-
 .../views/metadata/components/menu-tree/index.tsx  |  46 +++---
 .../metadata/components/metadata-tabs/index.tsx    |   3 +-
 .../components/options-form-content/index.tsx      |  22 +--
 .../index.tsx => options-form/edit.tsx}            |  61 ++++----
 .../metadata/components/options-form/index.tsx     | 164 +++++++++++++++++++++
 .../views/metadata/components/options/index.tsx    | 127 ++++++++++++++++
 .../src/views/metadata/components/schema/index.tsx |  35 ++---
 .../components/table-column-content/constant.ts    |   5 +-
 .../components/table-column-content/index.tsx      |  70 +++++++--
 .../views/metadata/components/table-form/index.tsx | 115 +++++++++++----
 .../metadata/{index.module.scss => constant.ts}    |  24 ++-
 .../src/views/metadata/index.module.scss           |   6 +-
 paimon-web-ui-new/src/views/metadata/index.tsx     |  20 ++-
 27 files changed, 744 insertions(+), 196 deletions(-)

diff --git a/paimon-web-ui-new/mock/modules/metadata.js b/paimon-web-ui-new/mock/modules/metadata.js
index 84e8a1e..0f13987 100644
--- a/paimon-web-ui-new/mock/modules/metadata.js
+++ b/paimon-web-ui-new/mock/modules/metadata.js
@@ -105,6 +105,24 @@ module.exports = (mockUtil) => ({
     "data": null
   }),
 
+  'post /metadata/query/options': mockUtil({
+    "code": 200,
+    "msg": "Successfully",
+    "data": [
+        {
+            "key": "bucket",
+            "value": "4"
+        },
+        {
+            "key": "changelog-producer",
+            "value": "input"
+        },
+        {
+            "key": "sink.parallelism",
+            "value": "4"
+        }
+    ]
+}),
   'post /metadata/query/schema': mockUtil({
     code: 200,
     msg: 'Successfully',
@@ -191,5 +209,16 @@ module.exports = (mockUtil) => ({
         "creationTime": "@now"
       }
     ]
+  }),
+
+  'post /table/option/add': mockUtil({
+    code: 200,
+    msg: 'Successfully',
+    data: null,
+  }),
+  'post /table/option/remove': mockUtil({
+    code: 200,
+    msg: 'Successfully',
+    data: null,
   })
 })
diff --git a/paimon-web-ui-new/package.json b/paimon-web-ui-new/package.json
index 95d51ca..9ccfd66 100644
--- a/paimon-web-ui-new/package.json
+++ b/paimon-web-ui-new/package.json
@@ -21,7 +21,6 @@
     "dayjs": "^1.11.10",
     "lodash": "^4.17.21",
     "mitt": "^3.0.1",
-    "mockm": "1.1.27-alpha.2",
     "monaco-editor": "^0.43.0",
     "pinia": "^2.1.6",
     "pinia-plugin-persistedstate": "^3.2.0",
diff --git a/paimon-web-ui-new/src/api/models/catalog/index.ts b/paimon-web-ui-new/src/api/models/catalog/index.ts
index 268f404..580ca76 100644
--- a/paimon-web-ui-new/src/api/models/catalog/index.ts
+++ b/paimon-web-ui-new/src/api/models/catalog/index.ts
@@ -15,6 +15,8 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
+import type { UseAxleOptions } from '@varlet/axle/use'
+
 import httpRequest from '@/api/request'
 import type {
   Catalog,
@@ -28,7 +30,10 @@ import type {
   CatalogDTO,
   DatabaseDTO,
   TableDTO,
-  SearchTable
+  TableParams,
+  SearchTable,
+  OptionsDTO,
+  TableOption
 } from './types'
 import type { ResponseOptions } from '@/api/types'
 
@@ -40,7 +45,7 @@ export * from './types'
  * # Get all catalog
  */
 export const getAllCatalogs = () => {
-  return httpRequest.get<unknown, ResponseOptions<Catalog[]>>('/catalog/list')
+  return httpRequest.get<unknown, ResponseOptions<Catalog[]>>('/catalog/getAllCatalogs')
 }
 
 /**
@@ -88,12 +93,41 @@ export const createTable = () => {
   })
 }
 
+/**
+ * # Get options
+ */
+export const getOptions = () => {
+  return httpRequest.createHooks!<TableOption[], TableParams>({
+    url: '/metadata/query/options',
+    method: 'post',
+  })
+}
+
+/**
+ * # Create new Option
+ */
+export const createOption = () => {
+  return httpRequest.createHooks!<unknown, TableDTO>({
+    url: '/table/option/add',
+    method: 'post'
+  })
+}
+
+/**
+ * # Delete option
+ */
+export const deleteOption = () => {
+  return httpRequest.createHooks!<TableOption[], TableQuery & { key: string; }>({
+    url: '/table/option/remove',
+    method: 'post',
+  })
+}
 
 /**
  * # Get schema
  */
 export const getSchema = () => {
-  return httpRequest.createHooks!<Schema[]>({
+  return httpRequest.createHooks!<Schema[], TableParams>({
     url: '/metadata/query/schema',
     method: 'post'
   })
@@ -103,7 +137,7 @@ export const getSchema = () => {
  * # Get manifest
  */
 export const getManifest = () => {
-  return httpRequest.createHooks!<Manifest[]>({
+  return httpRequest.createHooks!<Manifest[], TableParams>({
     url: '/metadata/query/manifest',
     method: 'post'
   })
@@ -113,7 +147,7 @@ export const getManifest = () => {
  * # Get data file
  */
 export const getDataFile = () => {
-  return httpRequest.createHooks!<Datafile[]>({
+  return httpRequest.createHooks!<Datafile[], TableParams>({
     url: '/metadata/query/dataFile',
     method: 'post'
   })
@@ -123,7 +157,7 @@ export const getDataFile = () => {
  * # Get snapshot
  */
 export const getSnapshot = () => {
-  return httpRequest.createHooks!<Snapshot[]>({
+  return httpRequest.createHooks!<Snapshot[], TableParams>({
     url: '/metadata/query/snapshot',
     method: 'post'
   })
diff --git a/paimon-web-ui-new/src/api/models/catalog/types/table.ts b/paimon-web-ui-new/src/api/models/catalog/types/table.ts
index 887052a..f352f0c 100644
--- a/paimon-web-ui-new/src/api/models/catalog/types/table.ts
+++ b/paimon-web-ui-new/src/api/models/catalog/types/table.ts
@@ -59,7 +59,7 @@ export interface ColumnDTO {
 }
 
 export interface DataTypeDTO {
-  type: string
+  type?: string
   precision?: number | null
   scale?: number | null
   nullable: boolean
@@ -70,6 +70,6 @@ export interface OptionsDTO {
 }
 
 export interface TableOption {
-  k: string;
-  v: string;
+  key: string;
+  value: string;
 }
diff --git a/paimon-web-ui-new/src/components/dynamic-form/index.tsx b/paimon-web-ui-new/src/components/dynamic-form/index.tsx
index 97064a1..a77c8be 100644
--- a/paimon-web-ui-new/src/components/dynamic-form/index.tsx
+++ b/paimon-web-ui-new/src/components/dynamic-form/index.tsx
@@ -15,7 +15,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-import type { FormRules, GridProps, IMeta } from "./types"
+import type { FormRules, GridProps, IFormInst, IMeta } from "./types"
 import { useFormHooks } from "./use-form"
 
 const props = {
@@ -36,11 +36,11 @@ const props = {
 export default defineComponent({
   name: 'DynamicForm',
   props,
-  setup(props, { expose }) {
+  setup(_, { expose }) {
     const { state, ...rest } = useFormHooks()
     expose({
       ...rest
-    })
+    }  as IFormInst)
     return { ...toRefs(state) }
   },
   render(props: { meta: IMeta; gridProps?: GridProps; loading?: boolean }) {
diff --git a/paimon-web-ui-new/src/components/dynamic-form/types.ts b/paimon-web-ui-new/src/components/dynamic-form/types.ts
index 56cd0e9..231a9a5 100644
--- a/paimon-web-ui-new/src/components/dynamic-form/types.ts
+++ b/paimon-web-ui-new/src/components/dynamic-form/types.ts
@@ -15,7 +15,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-import type { FormItemRule, FormProps, FormRules, GridProps } from "naive-ui"
+import type { FormInst, FormItemRule, FormProps, FormRules, GridProps } from "naive-ui"
 
 type ComponentType =
   | 'input'
@@ -71,6 +71,15 @@ interface IMeta extends Omit<FormProps, 'model' | 'rules'> {
   rules: IFormRules
 }
 
+interface IFormInst extends FormInst {
+  setValues: (initialValues: { [field: string]: any }) => void
+  resetValues: (initialValues: { [field: string]: any }) => void
+  getValues: () => { [field: string]: any }
+  restoreValidation: () => void
+  validate: (...args: []) => Promise<any>
+  formatValidate: (validate?: IFormItemRule | FormRules) => IFormItemRule
+}
+
 export type {
   IJsonItem,
   IJsonItemParams,
@@ -82,5 +91,6 @@ export type {
   IFormItemRule,
   IMeta,
   GridProps,
-  FormRules
+  FormRules,
+  IFormInst
 }
diff --git a/paimon-web-ui-new/src/components/dynamic-form/use-form.ts b/paimon-web-ui-new/src/components/dynamic-form/use-form.ts
index e508191..7c51a68 100644
--- a/paimon-web-ui-new/src/components/dynamic-form/use-form.ts
+++ b/paimon-web-ui-new/src/components/dynamic-form/use-form.ts
@@ -15,15 +15,16 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-import type { FormRules, IFormItemRule } from "./types"
+import type { FormInst } from 'naive-ui'
+import type { IFormInst, FormRules, IFormItemRule } from './types'
 
-export const useFormHooks = () => {
+export const useFormHooks = (): IFormInst & { state: { formRef: FormInst } } => {
   const state = reactive({
     formRef: ref()
   })
 
   const validate = async (...args: []) => {
-    await state.formRef.validate(...args)
+    return state.formRef.validate(...args)
   }
 
   const setValues = (initialValues: { [field: string]: any }) => {
diff --git a/paimon-web-ui-new/src/components/modal/index.tsx b/paimon-web-ui-new/src/components/modal/index.tsx
index a781e57..9f0d3a8 100644
--- a/paimon-web-ui-new/src/components/modal/index.tsx
+++ b/paimon-web-ui-new/src/components/modal/index.tsx
@@ -15,10 +15,10 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-import type { FormInst } from 'naive-ui'
+import Form from '@/components/dynamic-form'
+import type { IFormInst } from '@/components/dynamic-form/types'
 
-import Form from "@/components/dynamic-form"
-import { useTask } from "./use-task"
+import { useTask } from './use-task'
 
 const props = {
   title: {
@@ -53,7 +53,7 @@ export default defineComponent({
   emits: ['confirm', 'cancel'],
   setup(props, { expose, emit }) {
     const { t } = useLocaleHooks()
-    const formRef = ref<FormInst | null>()
+    const formRef = ref<IFormInst | null>()
     expose({ formRef })
 
     const { elementsRef, rulesRef, model } = useTask({
diff --git a/paimon-web-ui-new/src/form-lib/index.ts b/paimon-web-ui-new/src/form-lib/index.ts
index bea031f..18a3d2c 100644
--- a/paimon-web-ui-new/src/form-lib/index.ts
+++ b/paimon-web-ui-new/src/form-lib/index.ts
@@ -20,10 +20,12 @@ import { useMYSQL } from './cdc/use-mysql'
 import { usePaimon } from './cdc/use-paimon'
 
 import { useDatabase } from './metadata/use-database'
+import { useOptions } from './metadata/use-options'
 
 export default {
   CDCLIST: useCDCList,
   MYSQL: useMYSQL,
   PAIMON: usePaimon,
   DATABASE: useDatabase,
+  OPTIONS: useOptions
 }
diff --git a/paimon-web-ui-new/src/form-lib/metadata/use-options.ts b/paimon-web-ui-new/src/form-lib/metadata/use-options.ts
new file mode 100644
index 0000000..85daf1f
--- /dev/null
+++ b/paimon-web-ui-new/src/form-lib/metadata/use-options.ts
@@ -0,0 +1,65 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License. */
+
+import type { IJsonItem } from '@/components/dynamic-form/types'
+import type { TableOption } from '@/api/models/catalog'
+
+export function useOptions({ data }: { data?: TableOption }) {
+	const model = reactive(data ? data : { key: '', value: '' })
+
+	return {
+		json: [
+			{
+				type: 'input',
+				field: 'key',
+				name: 'Key',
+				props: {
+					placeholder: 'Key',
+					disabled: true,
+				},
+				validate: {
+					trigger: ['input', 'blur'],
+					required: true,
+					message: 'error',
+					validator: (validator: any, value: string) => {
+						if (!value) {
+							return new Error('Key is required')
+						}
+					}
+				}
+			},
+			{
+				type: 'input',
+				field: 'value',
+				name: 'Value',
+				props: {
+					placeholder: 'Value',
+				},
+				validate: {
+					trigger: ['input', 'blur'],
+					required: true,
+					message: 'error',
+					validator: (validator: any, value: string) => {
+						if (!value) {
+							return new Error('Value is required')
+						}
+					}
+				}
+			},
+		] as IJsonItem[], model
+	}
+}
diff --git a/paimon-web-ui-new/src/store/catalog/index.ts b/paimon-web-ui-new/src/store/catalog/index.ts
index 089c1c6..fc34bfa 100644
--- a/paimon-web-ui-new/src/store/catalog/index.ts
+++ b/paimon-web-ui-new/src/store/catalog/index.ts
@@ -93,7 +93,7 @@ const transformDatabase = (databases: Database[]): TreeOption[] => {
   return databases.map(database => ({
     label: database.name,
     type: 'database',
-    key: `${database.catalogId} ${database.name}`,
+    key: `${database.catalogId} ${database.catalogName} ${database.name}`,
     isLeaf: false,
   }))
 }
@@ -102,7 +102,7 @@ const transformTable = (tables: Table[]): TreeOption[] => {
   return tables.map(table => ({
     label: table.name,
     type: 'table',
-    key: `${table.catalogId} ${table.catalogName} ${table.databaseName} ${table.name}`,
+    key: `${JSON.stringify(table)}`,
     isLeaf: true,
     prefix: () =>
       h(NIcon, null, {
diff --git a/paimon-web-ui-new/src/views/metadata/components/catalog-form/index.tsx b/paimon-web-ui-new/src/views/metadata/components/catalog-form/index.tsx
index bccd693..279bf50 100644
--- a/paimon-web-ui-new/src/views/metadata/components/catalog-form/index.tsx
+++ b/paimon-web-ui-new/src/views/metadata/components/catalog-form/index.tsx
@@ -91,17 +91,7 @@ export default defineComponent({
       if (result.value.code === 200) {
         handleCloseModal()
         message.success(t('Create Successfully'))
-        formValue.value = {
-          name: '',
-          type: '',
-          warehouse: '',
-          options: {
-            fileSystemType: 'local',
-            endpoint: '',
-            accessKey: '',
-            secretKey: ''
-          }
-        }
+        resetState()
         catalogStore.getAllCatalogs(true)
       }
     }
@@ -113,6 +103,21 @@ export default defineComponent({
 
     const handleCloseModal = () => {
       showModal.value = false
+      resetState()
+    }
+
+    const resetState = () => {
+      formValue.value = {
+        name: '',
+        type: '',
+        warehouse: '',
+        options: {
+          fileSystemType: 'local',
+          endpoint: '',
+          accessKey: '',
+          secretKey: ''
+        }
+      }
     }
 
     return {
diff --git a/paimon-web-ui-new/src/views/metadata/components/database-form/index.tsx b/paimon-web-ui-new/src/views/metadata/components/database-form/index.tsx
index 99e4c74..f054759 100644
--- a/paimon-web-ui-new/src/views/metadata/components/database-form/index.tsx
+++ b/paimon-web-ui-new/src/views/metadata/components/database-form/index.tsx
@@ -16,11 +16,11 @@ specific language governing permissions and limitations
 under the License. */
 
 import { Add } from '@vicons/ionicons5'
-import type { FormInst } from 'naive-ui'
 
 import { createDatabase, type DatabaseFormDTO } from '@/api/models/catalog'
 import { useCatalogStore } from '@/store/catalog'
 import IModal from '@/components/modal'
+import type { IFormInst } from '@/components/dynamic-form/types'
 
 const props = {
   catalogId: {
@@ -39,7 +39,7 @@ export default defineComponent({
     const catalogStore = useCatalogStore()
     const [result, createFetch, { loading }] = createDatabase()
 
-    const modalRef = ref<{ formRef: FormInst }>()
+    const modalRef = ref<{ formRef: IFormInst }>()
     const showModal = ref(false)
 
     const handleConfirm = async (values: DatabaseFormDTO) => {
@@ -70,6 +70,9 @@ export default defineComponent({
 
     const handleCloseModal = () => {
       showModal.value = false
+      modalRef.value?.formRef?.resetValues({
+        name: '',
+      })
     }
 
     return {
diff --git a/paimon-web-ui-new/src/views/metadata/components/menu-tree/index.module.scss b/paimon-web-ui-new/src/views/metadata/components/menu-tree/index.module.scss
index 8926adc..9c58b31 100644
--- a/paimon-web-ui-new/src/views/metadata/components/menu-tree/index.module.scss
+++ b/paimon-web-ui-new/src/views/metadata/components/menu-tree/index.module.scss
@@ -24,12 +24,24 @@ under the License. */
   .card {
     height: 100%;
 
-    .search {
+    .vertical {
       display: flex;
-    }
+      flex-direction: column;
+      height: 100%;
+      gap: 20px;
 
-    .icon {
-      display: flex;
+      .search {
+        display: flex;
+      }
+  
+      .icon {
+        display: flex;
+      }
+
+      .scroll {
+        position: relative;
+        flex: 1;
+      }
     }
   }
 }
diff --git a/paimon-web-ui-new/src/views/metadata/components/menu-tree/index.tsx b/paimon-web-ui-new/src/views/metadata/components/menu-tree/index.tsx
index 0b9e189..c1060bb 100644
--- a/paimon-web-ui-new/src/views/metadata/components/menu-tree/index.tsx
+++ b/paimon-web-ui-new/src/views/metadata/components/menu-tree/index.tsx
@@ -57,7 +57,7 @@ export default defineComponent({
           return h(DatabaseFormButton, { catalogId: Number(catalogId) })
         case 'database':
           const [id, name, databaseName] = option.key?.toString()?.split(' ') || []
-          return h(TableFormButton, { catalogId: id, catalogName: name, databaseName })
+          return h(TableFormButton, { catalogId: Number(id), catalogName: name, databaseName })
         default:
           return undefined
       }
@@ -83,9 +83,10 @@ export default defineComponent({
       if (node.type === 'catalog') {
         node.children = await catalogStore.getDatabasesById(node.key as number)
       } else {
-        const [catalogId, databaseName] = (node.key as string)?.split(' ') || []
+        const [catalogId, catalogName, databaseName] = (node.key as string)?.split(' ') || []
         const params = {
           catalogId: Number(catalogId),
+          catalogName,
           databaseName
         }
         node.children = (await catalogStore.getTablesByDataBaseId(params)) || []
@@ -99,13 +100,12 @@ export default defineComponent({
         onClick() {
           const { type } = option
           if (type === 'table') {
-            const [catalogId, catalogName, databaseName, name] =
-              (option.key?.toString() || '')?.split(' ') || []
+            const { catalogId, name, ...tableData } = JSON.parse(option.key?.toString() || '')
             catalogStore.setCurrentTable({
               catalogId: Number(catalogId),
-              catalogName,
-              databaseName,
-              tableName: name
+              tableName: name,
+              name,
+              ...tableData
             })
           }
         }
@@ -139,7 +139,7 @@ export default defineComponent({
     return (
       <div class={styles.container}>
         <n-card class={styles.card} content-style={'padding:20px 18px;'}>
-          <n-space vertical>
+          <div class={styles.vertical}>
             <n-space justify="space-between" align="enter">
               <article>Catalog</article>
               <CatalogFormButton />
@@ -153,19 +153,23 @@ export default defineComponent({
               }}
               onKeyup={this.onSearch}
             ></n-input>
-            <n-spin show={this.menuLoading}>
-              <n-tree
-                block-line
-                expand-on-click
-                data={this.menuList}
-                defaultExpandAll={this.isSearch}
-                nodeProps={this.nodeProps}
-                renderSuffix={this.renderSuffix}
-                renderSwitcherIcon={this.renderPrefix}
-                onLoad={this.onLoadMenu}
-              />
-            </n-spin>
-          </n-space>
+            <div class={styles.scroll}>
+              <n-scrollbar style={'position: absolute'}>
+                <n-spin show={this.menuLoading}>
+                  <n-tree
+                    block-line
+                    expand-on-click
+                    data={this.menuList}
+                    defaultExpandAll={this.isSearch}
+                    nodeProps={this.nodeProps}
+                    renderSuffix={this.renderSuffix}
+                    renderSwitcherIcon={this.renderPrefix}
+                    onLoad={this.onLoadMenu}
+                  />
+                </n-spin>
+              </n-scrollbar>
+            </div>
+          </div>
         </n-card>
       </div>
     )
diff --git a/paimon-web-ui-new/src/views/metadata/components/metadata-tabs/index.tsx b/paimon-web-ui-new/src/views/metadata/components/metadata-tabs/index.tsx
index 6704ce0..027269d 100644
--- a/paimon-web-ui-new/src/views/metadata/components/metadata-tabs/index.tsx
+++ b/paimon-web-ui-new/src/views/metadata/components/metadata-tabs/index.tsx
@@ -20,6 +20,7 @@ import type { TabsInst } from 'naive-ui'
 import { useConfigStore } from '@/store/config'
 
 import Table from '../table'
+import Options from '../options'
 import Schema from '../schema'
 import SnapShot from '../snapshot'
 import Manifest from '../manifest'
@@ -52,7 +53,7 @@ export default defineComponent({
             <Table />
           </n-tab-pane>
           <n-tab-pane name='option' tab={this.t('metadata.option_info')}>
-            Option
+            <Options />
           </n-tab-pane>
           <n-tab-pane name='schema' tab={this.t('metadata.schema_info')}>
             <Schema />
diff --git a/paimon-web-ui-new/src/views/metadata/components/options-form-content/index.tsx b/paimon-web-ui-new/src/views/metadata/components/options-form-content/index.tsx
index 051f633..44ad630 100644
--- a/paimon-web-ui-new/src/views/metadata/components/options-form-content/index.tsx
+++ b/paimon-web-ui-new/src/views/metadata/components/options-form-content/index.tsx
@@ -15,6 +15,8 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
+import { RemoveCircleOutline } from '@vicons/ionicons5'
+
 import type { TableOption } from '@/api/models/catalog'
 
 const props = {
@@ -26,8 +28,8 @@ const props = {
 }
 
 export const newOption: TableOption = {
-  k: '',
-  v: ''
+  key: '',
+  value: ''
 }
 
 export default defineComponent({
@@ -53,30 +55,32 @@ export default defineComponent({
             <n-form-item-gi
               span={11}
               showLabel={false}
-              path={`options[${index}].k`}
+              path={`options[${index}].key`}
               rule={{
                 required: true,
                 message: 'Key is required',
                 trigger: ['input', 'blur']
               }}
             >
-              <n-input v-model:value={item.k} placeholder="Key" />
+              <n-input v-model:value={item.key} placeholder="Key" />
             </n-form-item-gi>
             <n-form-item-gi
               span={11}
               showLabel={false}
-              path={`options[${index}].v`}
+              path={`options[${index}].value`}
               rule={{
                 required: true,
                 message: 'Value is required',
                 trigger: ['input', 'blur']
               }}
             >
-              <n-input v-model:value={item.v} placeholder="Value" />
+              <n-input v-model:value={item.value} placeholder="Value" />
             </n-form-item-gi>
-            <n-gi suffix block span={2} class="suffix">
-              <n-button onClick={() => this.onDelete(index)} tertiary type="error">
-                Remove
+            <n-gi span={2}>
+              <n-button block onClick={() => this.onDelete(index)} tertiary type="error">
+                {{
+                  icon: () => <n-icon component={RemoveCircleOutline} />
+                }}
               </n-button>
             </n-gi>
           </>
diff --git a/paimon-web-ui-new/src/views/metadata/components/database-form/index.tsx b/paimon-web-ui-new/src/views/metadata/components/options-form/edit.tsx
similarity index 58%
copy from paimon-web-ui-new/src/views/metadata/components/database-form/index.tsx
copy to paimon-web-ui-new/src/views/metadata/components/options-form/edit.tsx
index 99e4c74..8443161 100644
--- a/paimon-web-ui-new/src/views/metadata/components/database-form/index.tsx
+++ b/paimon-web-ui-new/src/views/metadata/components/options-form/edit.tsx
@@ -15,51 +15,45 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-import { Add } from '@vicons/ionicons5'
-import type { FormInst } from 'naive-ui'
+import { CreateOutline } from '@vicons/ionicons5'
 
-import { createDatabase, type DatabaseFormDTO } from '@/api/models/catalog'
+import { createOption, type TableOption } from '@/api/models/catalog'
 import { useCatalogStore } from '@/store/catalog'
 import IModal from '@/components/modal'
+import type { IFormInst } from '@/components/dynamic-form/types'
+import { transformOption } from '@/views/metadata/constant'
 
 const props = {
-  catalogId: {
-    type: Number as PropType<number>,
-    require: true
-  }
+  value: Object as PropType<TableOption>,
+  onConfirm: [Function, Array] as PropType<() => Promise<void>>
 }
 
 export default defineComponent({
-  name: 'DatabaseForm',
+  name: 'OptionEditForm',
   props,
   setup(props) {
     const { t } = useLocaleHooks()
     const message = useMessage()
 
     const catalogStore = useCatalogStore()
-    const [result, createFetch, { loading }] = createDatabase()
+    const [result, createFetch, { loading }] = createOption()
 
-    const modalRef = ref<{ formRef: FormInst }>()
+    const modalRef = ref<{ formRef: IFormInst }>()
     const showModal = ref(false)
 
-    const handleConfirm = async (values: DatabaseFormDTO) => {
+    const handleConfirm = async (values: TableOption) => {
       await modalRef.value?.formRef?.validate()
-      if (props.catalogId) {
-        await createFetch({
-          params: {
-            ...values,
-            catalogId: props.catalogId
-          }
+      await createFetch({
+        params: transformOption({
+          ...toRaw(catalogStore.currentTable),
+          options: [toRaw(values)]
         })
+      })
 
-        if (result.value.code === 200) {
-          handleCloseModal()
-          message.success(t('Create Successfully'))
-          values = reactive({
-            name: '',
-          })
-          catalogStore.getAllCatalogs(true)
-        }
+      if (result.value.code === 200) {
+        handleCloseModal()
+        message.success(t('Create Successfully'))
+        props.onConfirm!()
       }
     }
 
@@ -70,11 +64,13 @@ export default defineComponent({
 
     const handleCloseModal = () => {
       showModal.value = false
+      props.onConfirm!()
     }
 
     return {
       modalRef,
       showModal,
+      value: toRefs(props).value,
 
       t,
       handleOpenModal,
@@ -85,16 +81,17 @@ export default defineComponent({
   render() {
     return (
       <>
-        <n-button quaternary circle size="tiny" onClick={this.handleOpenModal}>
-          <n-icon>
-            <Add />
-          </n-icon>
+        <n-button onClick={this.handleOpenModal} strong secondary circle>
+          {{
+            icon: () => <n-icon component={CreateOutline} />
+          }}
         </n-button>
         <IModal
-          ref="modalRef"
+          ref='modalRef'
           showModal={this.showModal}
-          title={this.t(`metadata.create_database`)}
-          formType={'DATABASE'}
+          row={this.value}
+          title={'Edit Option'}
+          formType={'OPTIONS'}
           onCancel={this.handleCloseModal}
           onConfirm={this.handleConfirm}
         />
diff --git a/paimon-web-ui-new/src/views/metadata/components/options-form/index.tsx b/paimon-web-ui-new/src/views/metadata/components/options-form/index.tsx
new file mode 100644
index 0000000..8323049
--- /dev/null
+++ b/paimon-web-ui-new/src/views/metadata/components/options-form/index.tsx
@@ -0,0 +1,164 @@
+/* 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 { Add, AddCircleOutline } from '@vicons/ionicons5'
+
+import { useCatalogStore } from '@/store/catalog'
+import { createOption, type TableOption } from '@/api/models/catalog'
+import { transformOption } from '@/views/metadata/constant'
+
+import OptionContent, { newOption } from '../options-form-content'
+
+type OptionsFormType = {
+  options: TableOption[]
+}
+
+const props = {
+  onConfirm: [Function, Array] as PropType<() => Promise<void>>
+}
+
+export default defineComponent({
+  name: 'OptionForm',
+  props,
+  setup(props) {
+    const rules = {
+      key: {
+        required: true,
+        trigger: ['blur', 'input'],
+        message: 'Option key required'
+      },
+      value: {
+        required: true,
+        trigger: ['blur', 'input'],
+        message: 'Option value required'
+      }
+    }
+
+    const { t } = useLocaleHooks()
+    const message = useMessage()
+
+    const catalogStore = useCatalogStore()
+    const [result, createFetch, { loading }] = createOption()
+
+    const formRef = ref()
+    const formValue = ref<OptionsFormType>({
+      options: [{ ...newOption }]
+    })
+    const showModal = ref(false)
+
+    const handleConfirm = async () => {
+      await formRef.value.validate()
+      await createFetch({
+        params: transformOption({
+          ...toRaw(catalogStore.currentTable),
+          options: toRaw(formValue.value).options
+        })
+      })
+
+      if (result.value.code === 200) {
+        handleCloseModal()
+        message.success(t('Create Successfully'))
+        resetState()
+        props.onConfirm!()
+      }
+    }
+
+    const handleOpenModal = (e: Event) => {
+      e.stopPropagation()
+      showModal.value = true
+    }
+
+    const handleCloseModal = () => {
+      showModal.value = false
+      resetState()
+    }
+
+    const resetState = () => {
+      formValue.value = {
+        options: [{ ...newOption }]
+      }
+    }
+
+    const handleAddOption = () => {
+      formValue.value?.options.push({ ...newOption })
+    }
+
+    return {
+      formRef,
+      formValue,
+      showModal,
+      loading,
+
+      rules,
+
+      t,
+      handleOpenModal,
+      handleCloseModal,
+      handleConfirm,
+      handleAddOption
+    }
+  },
+  render() {
+    return (
+      <>
+        <n-button strong secondary circle onClick={this.handleOpenModal}>
+          {{
+            icon: () => <n-icon component={AddCircleOutline} />
+          }}
+        </n-button>
+        <n-modal v-model:show={this.showModal} mask-closable={false}>
+          <n-card bordered={true} title={'Create Option'} style="width: 700px">
+            {{
+              'header-extra': () => (
+                <n-button quaternary circle size="tiny" onClick={this.handleAddOption}>
+                  <n-icon>
+                    <Add />
+                  </n-icon>
+                </n-button>
+              ),
+              default: () => (
+                <n-form
+                  ref="formRef"
+                  label-placement="top"
+                  label-width="auto"
+                  label-align="left"
+                  rules={this.rules}
+                  model={this.formValue}
+                >
+                  <OptionContent
+                    onUpdateOptions={(value) => {
+                      this.formValue.options = value
+                    }}
+                    options={this.formValue.options || []}
+                  />
+                </n-form>
+              ),
+              action: () => (
+                <n-space justify="end">
+                  <n-button onClick={this.handleCloseModal}>{this.t('layout.cancel')}</n-button>
+                  <n-button type="primary" loading={this.loading} onClick={this.handleConfirm}>
+                    {this.t('layout.confirm')}
+                  </n-button>
+                </n-space>
+              )
+            }}
+          </n-card>
+        </n-modal>
+      </>
+    )
+  }
+})
diff --git a/paimon-web-ui-new/src/views/metadata/components/options/index.tsx b/paimon-web-ui-new/src/views/metadata/components/options/index.tsx
new file mode 100644
index 0000000..1c53f82
--- /dev/null
+++ b/paimon-web-ui-new/src/views/metadata/components/options/index.tsx
@@ -0,0 +1,127 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License. */
+
+import { type DataTableColumns } from 'naive-ui'
+import { RemoveCircleOutline, Warning } from '@vicons/ionicons5'
+
+import { deleteOption, getOptions, type TableOption } from '@/api/models/catalog'
+import { useCatalogStore } from '@/store/catalog'
+import OptionsForm from '../options-form'
+import OptionsEditForm from '../options-form/edit'
+
+export default defineComponent({
+  name: 'MetadataOptions',
+  setup() {
+    const { t } = useLocaleHooks()
+    const catalogStore = useCatalogStore()
+
+    const [optionsList, useOptionsList, { loading }] = getOptions()
+    const [, useDelete] = deleteOption()
+
+    const columns: DataTableColumns<TableOption> = [
+      {
+        title: 'Key',
+        key: 'key'
+      },
+      {
+        title: 'Value',
+        key: 'value'
+      },
+      {
+        title: 'Operation',
+        key: 'operation',
+        render(rowData) {
+          return (
+            <n-space>
+              <OptionsEditForm onConfirm={onFetchData} value={rowData} />
+              <n-popconfirm onPositiveClick={() => onDeleteOption(rowData?.key)}>
+                {{
+                  default: () => 'Confirm to delete ? ',
+                  trigger: () => (
+                    <n-button strong secondary circle type="error">
+                      {{
+                        icon: () => <n-icon component={RemoveCircleOutline} />
+                      }}
+                    </n-button>
+                  ),
+                  icon: () => <n-icon color='#EC4C4D' component={Warning} />
+                }}
+              </n-popconfirm>
+            </n-space>
+          )
+        }
+      }
+    ]
+
+    const onFetchData = async () => {
+      useOptionsList({
+        params: catalogStore.currentTable
+      })
+    }
+
+    const onDeleteOption = async (optionKey: string) => {
+      await useDelete({
+        config: {
+          params: {
+            ...toRaw(catalogStore.currentTable),
+            key: optionKey
+          }
+        }
+      })
+
+      await onFetchData()
+
+      return Promise.resolve(true)
+    }
+
+    watch(() => catalogStore.currentTable, onFetchData)
+
+    onMounted(onFetchData)
+
+    return {
+      columns,
+      optionsList,
+      loading,
+      pagination: {
+        pageSize: 10
+      },
+
+      onFetchData,
+      t
+    }
+  },
+  render() {
+    return (
+      <n-spin show={this.loading}>
+        <n-card title="Options">
+          {{
+            'header-extra': () => (
+              <OptionsForm onConfirm={this.onFetchData} />
+            ),
+            default: () => (
+              <n-data-table
+                columns={this.columns}
+                data={this.optionsList?.data || []}
+                pagination={this.pagination}
+              />
+            )
+          }}
+        </n-card>
+      </n-spin>
+    )
+  }
+})
diff --git a/paimon-web-ui-new/src/views/metadata/components/schema/index.tsx b/paimon-web-ui-new/src/views/metadata/components/schema/index.tsx
index faa2b41..bc0aa04 100644
--- a/paimon-web-ui-new/src/views/metadata/components/schema/index.tsx
+++ b/paimon-web-ui-new/src/views/metadata/components/schema/index.tsx
@@ -28,7 +28,7 @@ export default defineComponent({
   name: 'MetadataSchema',
   setup() {
     const { t } = useLocaleHooks()
-    
+
     const catalogStore = useCatalogStore()
 
     const [schemaData, useSchemaInfo, { loading }] = getSchema()
@@ -40,25 +40,19 @@ export default defineComponent({
         renderExpand: (row) => {
           const { fields = [], option = [] } = row
 
-          return <div>
-            <div class={styles.schema_field_title}>Fields: </div>
-            <n-data-table
-              columns={fieldsColumns}
-              data={fields}
-              bordered={false}
-            />
-            <div class={styles.schema_field_title}>Options: </div>
-            <n-data-table
-              columns={optionsColumns}
-              data={option}
-              bordered={false}
-            />
-          </div>
+          return (
+            <div>
+              <div class={styles.schema_field_title}>Fields: </div>
+              <n-data-table columns={fieldsColumns} data={fields} bordered={false} />
+              <div class={styles.schema_field_title}>Options: </div>
+              <n-data-table columns={optionsColumns} data={option} bordered={false} />
+            </div>
+          )
         }
       },
       {
         title: 'ID',
-        key: 'schemaId',
+        key: 'schemaId'
       },
       {
         title: 'Partition Keys',
@@ -99,13 +93,12 @@ export default defineComponent({
       columns,
       schemaData,
       loading,
-      t,
+      t
     }
   },
   render() {
     return (
       <n-card>
-        {this.loading}
         <n-spin show={this.loading}>
           <n-data-table
             row-key={(rowData: Schema) => rowData.schemaId}
@@ -115,6 +108,6 @@ export default defineComponent({
           />
         </n-spin>
       </n-card>
-    );
-  },
-});
+    )
+  }
+})
diff --git a/paimon-web-ui-new/src/views/metadata/components/table-column-content/constant.ts b/paimon-web-ui-new/src/views/metadata/components/table-column-content/constant.ts
index ebf27a4..430ae20 100644
--- a/paimon-web-ui-new/src/views/metadata/components/table-column-content/constant.ts
+++ b/paimon-web-ui-new/src/views/metadata/components/table-column-content/constant.ts
@@ -15,7 +15,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-export const options = [
+export const dataTypeOptions = [
   { value: 'INT', label: 'INT' },
   { value: 'TINYINT', label: 'TINYINT' },
   { value: 'SMALLINT', label: 'SMALLINT' },
@@ -40,3 +40,6 @@ export const options = [
   { value: 'TIMESTAMP_WITH_LOCAL_TIME_ZONE(precision)', label: 'TIMESTAMP_WITH_LOCAL_TIME_ZONE(precision)' },
 ];
 
+export const hasLength = ['CHAR', 'VARCHAR', 'BINARY', 'VARBINARY', 'TIME(precision)', 'TIMESTAMP(precision)', 'TIMESTAMP_WITH_LOCAL_TIME_ZONE(precision)', 'DECIMAL'];
+
+export const hasEndLength = ['DECIMAL'];
diff --git a/paimon-web-ui-new/src/views/metadata/components/table-column-content/index.tsx b/paimon-web-ui-new/src/views/metadata/components/table-column-content/index.tsx
index e1c2352..8975b0d 100644
--- a/paimon-web-ui-new/src/views/metadata/components/table-column-content/index.tsx
+++ b/paimon-web-ui-new/src/views/metadata/components/table-column-content/index.tsx
@@ -19,6 +19,8 @@ import type { DataTableColumns } from 'naive-ui'
 
 import type { ColumnDTO } from '@/api/models/catalog'
 
+import { hasLength, hasEndLength, dataTypeOptions } from './constant'
+
 const props = {
   data: {
     type: Array as PropType<ColumnDTO[]>,
@@ -27,6 +29,17 @@ const props = {
   onUpdateColumns: [Function, Array] as PropType<((value: ColumnDTO[]) => void) | undefined>
 }
 
+export const newField: ColumnDTO = {
+  field: '',
+  dataType: {
+    nullable: true,
+    type: undefined
+  },
+  comment: '',
+  defaultValue: '',
+  pk: false
+}
+
 export default defineComponent({
   name: 'OptionFormContent',
   props,
@@ -80,7 +93,11 @@ export default defineComponent({
                 trigger: ['input', 'blur']
               }}
             >
-              <n-select v-model:value={row.dataType.type} />
+              <n-select
+                placeholder={t('metadata.column_type')}
+                v-model:value={row.dataType.type}
+                options={dataTypeOptions}
+              />
             </n-form-item>
           )
         }
@@ -88,20 +105,41 @@ export default defineComponent({
       {
         title: t('metadata.column_length'),
         key: 'length',
-        width: 150,
+        width: 190,
         render: (row, index) => {
+          const hasStartLength = hasLength.includes(row.dataType.type || '')
+          const hasScaleLength = hasEndLength.includes(row.dataType.type || '')
           return (
-            <n-form-item
-              show-feedback={false}
-              showLabel={false}
-              path={`tableColumns[${index}].dataType`}
-              rule={{
-                required: true,
-                trigger: ['input', 'blur']
-              }}
-            >
-              <n-select v-model:value={row.dataType} />
-            </n-form-item>
+            <n-space wrap={false}>
+              {hasStartLength && (
+                <n-form-item
+                  show-feedback={false}
+                  showLabel={false}
+                  path={`tableColumns[${index}].dataType.precision`}
+                  rule={{
+                    type: 'number',
+                    required: true,
+                    trigger: ['input', 'blur']
+                  }}
+                >
+                  <n-input-number v-model:value={row.dataType.precision} show-button={false} />
+                </n-form-item>
+              )}
+              {hasScaleLength && (
+                <n-form-item
+                  show-feedback={false}
+                  showLabel={false}
+                  path={`tableColumns[${index}].dataType.scale`}
+                  rule={{
+                    type: 'number',
+                    required: true,
+                    trigger: ['input', 'blur']
+                  }}
+                >
+                  <n-input-number v-model:value={row.dataType.scale} show-button={false} />
+                </n-form-item>
+              )}
+            </n-space>
           )
         }
       },
@@ -112,14 +150,14 @@ export default defineComponent({
         render: (row, index) => {
           return (
             <n-form-item show-feedback={false} showLabel={false} path={`tableColumns[${index}].pk`}>
-              <n-checkbox v-model:value={row.pk} />
+              <n-checkbox v-model:checked={row.pk} />
             </n-form-item>
           )
         }
       },
       {
         title: t('metadata.column_nullable'),
-        key: 'nullable',
+        key: 'dataType.nullable',
         width: 80,
         render: (row, index) => {
           return (
@@ -128,7 +166,7 @@ export default defineComponent({
               showLabel={false}
               path={`tableColumns[${index}].dataType.nullable`}
             >
-              <n-checkbox v-model:value={row.dataType.nullable} />
+              <n-checkbox v-model:checked={row.dataType.nullable} />
             </n-form-item>
           )
         }
diff --git a/paimon-web-ui-new/src/views/metadata/components/table-form/index.tsx b/paimon-web-ui-new/src/views/metadata/components/table-form/index.tsx
index 6044a86..182d6dc 100644
--- a/paimon-web-ui-new/src/views/metadata/components/table-form/index.tsx
+++ b/paimon-web-ui-new/src/views/metadata/components/table-form/index.tsx
@@ -18,16 +18,54 @@ under the License. */
 import { Add } from '@vicons/ionicons5'
 
 import { useCatalogStore } from '@/store/catalog'
-import { createTable, type TableDTO, type TableOption } from '@/api/models/catalog'
+import { createTable, type TableDTO } from '@/api/models/catalog'
+import { transformOption } from '@/views/metadata/constant'
 
 import OptionContent, { newOption } from '../options-form-content'
-import TableColumnContent from '../table-column-content'
+import TableColumnContent, { newField } from '../table-column-content'
 
 import styles from './index.module.scss'
 
+
+const props = {
+  catalogId: {
+    type: Number as PropType<number>,
+    require: true
+  },
+  catalogName: {
+    type: String as PropType<string>,
+    require: true
+  },
+  databaseName: {
+    type: String as PropType<string>,
+    require: true
+  }
+}
+
+
+const resetFormValue = () => {
+  return {
+    name: '',
+    tableColumns: [
+      {
+        field: '',
+        dataType: {
+          nullable: true,
+          type: undefined
+        },
+        comment: '',
+        defaultValue: '',
+        pk: false
+      }
+    ],
+    options: []
+  }
+}
+
 export default defineComponent({
   name: 'TableForm',
-  setup() {
+  props,
+  setup(props) {
     const rules = {}
 
     const { t } = useLocaleHooks()
@@ -37,37 +75,35 @@ export default defineComponent({
     const [result, createFetch, { loading }] = createTable()
 
     const formRef = ref()
-    const formValue = ref<TableDTO>({
-      name: '',
-      tableColumns: [
-        {
-          field: '',
-          dataType: {
-            type: '',
-            nullable: true
-          },
-          comment: '',
-          defaultValue: '',
-          pk: false
-        }
-      ],
-      options: []
-    })
+    const formValue = ref<TableDTO>(resetFormValue())
     const showModal = ref(false)
 
+    const tableKeys = computed(() => {
+      return formValue.value
+        .tableColumns!.filter((item) => Boolean(item.field))
+        .map((item) => {
+          return {
+            label: item.field,
+            value: item.field
+          }
+        })
+    })
+
     const handleConfirm = async () => {
       await formRef.value.validate()
+
       await createFetch({
-        params: toRaw(formValue.value)
+        params: {
+          ...toRaw(props),
+          ...transformOption(toRaw(formValue.value))
+        }
       })
 
       if (result.value.code === 200) {
         handleCloseModal()
         message.success(t('Create Successfully'))
-        formValue.value = {
-          name: ''
-        }
-        catalogStore.getAllCatalogs(true)
+        formValue.value = resetFormValue()
+        await catalogStore.getAllCatalogs(true)
       }
     }
 
@@ -78,25 +114,33 @@ export default defineComponent({
 
     const handleCloseModal = () => {
       showModal.value = false
+      formValue.value = resetFormValue()
     }
 
     const handleAddOption = () => {
       formValue.value.options?.push({ ...newOption })
     }
 
+    const handleAddColumn = () => {
+      formValue.value.tableColumns?.push(JSON.parse(JSON.stringify(newField)))
+    }
+
+
     return {
       formRef,
       formValue,
       showModal,
       loading,
 
+      tableKeys,
       rules,
 
       t,
       handleOpenModal,
       handleCloseModal,
       handleConfirm,
-      handleAddOption
+      handleAddOption,
+      handleAddColumn
     }
   },
   render() {
@@ -113,16 +157,24 @@ export default defineComponent({
               default: () => (
                 <n-form ref="formRef" rules={this.rules} model={this.formValue}>
                   <div class={styles.form_title}>{this.t('metadata.table_basic_information')}</div>
-                  <n-form-item label={this.t('metadata.table_name')} path="name">
+                  <n-form-item
+                    rule={{
+                      required: true,
+                      message: 'Name is required',
+                      trigger: ['input', 'blur']
+                    }}
+                    label={this.t('metadata.table_name')}
+                    path="name"
+                  >
                     <n-input v-model:value={this.formValue.name} />
                   </n-form-item>
                   <n-form-item label={this.t('metadata.table_des')} path="type">
-                    <n-input type="textarea" />
+                    <n-input v-model:value={this.formValue.description} type="textarea" />
                   </n-form-item>
 
                   <n-space align="center" justify="space-between" class={styles.form_title}>
                     {this.t('metadata.table_columns')}
-                    <n-button circle onClick={this.handleOpenModal}>
+                    <n-button circle onClick={this.handleAddColumn}>
                       <n-icon>
                         <Add />
                       </n-icon>
@@ -136,7 +188,12 @@ export default defineComponent({
                   />
                   <div class={styles.form_title}>{this.t('metadata.partition_columns')}</div>
                   <n-form-item showLabel={false} path="type">
-                    <n-select placeholder="Select" />
+                    <n-select
+                      multiple
+                      options={this.tableKeys}
+                      v-model:value={this.formValue.partitionKey}
+                      placeholder="Select"
+                    />
                   </n-form-item>
                   <n-space align="center" justify="space-between" class={styles.form_title}>
                     {this.t('metadata.table_add_options')}
diff --git a/paimon-web-ui-new/src/views/metadata/index.module.scss b/paimon-web-ui-new/src/views/metadata/constant.ts
similarity index 73%
copy from paimon-web-ui-new/src/views/metadata/index.module.scss
copy to paimon-web-ui-new/src/views/metadata/constant.ts
index 458f8d8..4bc583a 100644
--- a/paimon-web-ui-new/src/views/metadata/index.module.scss
+++ b/paimon-web-ui-new/src/views/metadata/constant.ts
@@ -15,21 +15,15 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-.container {
-  display: flex;
-  width: 100%;
-  height: 100%;
+import type { OptionsDTO, TableDTO } from "@/api/models/catalog"
 
-  .content {
-    display: flex;
-    flex-direction: column;
-    
-    width: calc(100% - 60px);
-
-    padding: 14px 20px;
-
-    :global(.n-empty) {
-      margin-top: 200px;
-    }
+export const transformOption = (value: TableDTO) => {
+  const tableOptions: OptionsDTO = {}
+  value.options?.forEach((item) => {
+    tableOptions[item.key] = item.value
+  })
+  return {
+    ...value,
+    tableOptions
   }
 }
diff --git a/paimon-web-ui-new/src/views/metadata/index.module.scss b/paimon-web-ui-new/src/views/metadata/index.module.scss
index 458f8d8..91473a5 100644
--- a/paimon-web-ui-new/src/views/metadata/index.module.scss
+++ b/paimon-web-ui-new/src/views/metadata/index.module.scss
@@ -28,8 +28,10 @@ under the License. */
 
     padding: 14px 20px;
 
-    :global(.n-empty) {
-      margin-top: 200px;
+    .empty {
+      :global(.n-empty) {
+        margin-top: 200px;
+      }
     }
   }
 }
diff --git a/paimon-web-ui-new/src/views/metadata/index.tsx b/paimon-web-ui-new/src/views/metadata/index.tsx
index 13917f0..5b09581 100644
--- a/paimon-web-ui-new/src/views/metadata/index.tsx
+++ b/paimon-web-ui-new/src/views/metadata/index.tsx
@@ -18,7 +18,7 @@ under the License. */
 import { useCatalogStore } from '@/store/catalog'
 
 import styles from './index.module.scss'
-import MenuTree from "./components/menu-tree"
+import MenuTree from './components/menu-tree'
 import Breadcrumb from './components/breadcrumb'
 import Tabs from './components/metadata-tabs'
 
@@ -37,14 +37,18 @@ export default defineComponent({
       <div class={styles.container}>
         <MenuTree />
         <div class={styles.content}>
-          {
-            this.currentTable ? <>
+          {this.currentTable ? (
+            <>
               <Breadcrumb />
               <Tabs />
-            </> : <n-empty description='Select the table. Please'></n-empty>
-          }
+            </>
+          ) : (
+            <div class={styles.empty}>
+              <n-empty description="Select the table. Please"></n-empty>
+            </div>
+          )}
         </div>
       </div>
-    );
-  },
-});
+    )
+  }
+})