This is an automated email from the ASF dual-hosted git repository.

jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new d64c514f0 [#5965] subtask(web): ML model support for web UI (#6025)
d64c514f0 is described below

commit d64c514f02e3fdd427d3f42c368875b621e52dfa
Author: Qian Xia <[email protected]>
AuthorDate: Mon Dec 30 15:54:49 2024 +0800

    [#5965] subtask(web): ML model support for web UI (#6025)
    
    ### What changes were proposed in this pull request?
    create catalog
    <img width="1410" alt="image"
    
src="https://github.com/user-attachments/assets/45311026-9458-436b-869c-5d6190641ba7";
    />
    
    create schema
    <img width="1424" alt="image"
    
src="https://github.com/user-attachments/assets/7c1c82b8-2aae-4614-8f02-cceca1a49ae9";
    />
    
    register/view/drop/list model
    <img width="1420" alt="image"
    
src="https://github.com/user-attachments/assets/bbb2ae41-0153-4d8d-af24-e182f16a30e3";
    />
    <img width="1469" alt="image"
    
src="https://github.com/user-attachments/assets/e3bf371d-5e48-4f70-a9a1-4faa1f821c3c";
    />
    
    link/view/drop/list versions
    <img width="1445" alt="image"
    
src="https://github.com/user-attachments/assets/050a2f15-222c-484a-9abe-edcc5335c606";
    />
    <img width="1462" alt="image"
    
src="https://github.com/user-attachments/assets/0f7de28f-1614-40ca-8842-fd79ea68871b";
    />
    <img width="1429" alt="image"
    
src="https://github.com/user-attachments/assets/9a01f634-7113-4553-ad30-9d6e2ed9adc9";
    />
    <img width="1409" alt="image"
    
src="https://github.com/user-attachments/assets/f5292e23-6b90-4b21-b1d7-19c9fe72ba61";
    />
    
    
    ### Why are the changes needed?
    
    (Please clarify why the changes are needed. For instance,
      1. If you propose a new API, clarify the use case for a new API.
      2. If you fix a bug, describe the bug.)
    
    Fix: #5817
    
    ### Does this PR introduce _any_ user-facing change?
    N/A
    
    ### How was this patch tested?
    manually
---
 docs/assets/webui/create-table.png                 | Bin 0 -> 238896 bytes
 docs/assets/webui/create-topic.png                 | Bin 0 -> 223540 bytes
 docs/assets/webui/delete-model.png                 | Bin 0 -> 200946 bytes
 docs/assets/webui/delete-table.png                 | Bin 0 -> 266515 bytes
 docs/assets/webui/delete-topic.png                 | Bin 0 -> 215120 bytes
 docs/assets/webui/delete-version.png               | Bin 0 -> 199603 bytes
 docs/assets/webui/link-version.png                 | Bin 0 -> 201414 bytes
 docs/assets/webui/list-columns.png                 | Bin 229252 -> 229252 bytes
 docs/assets/webui/list-model-versions.png          | Bin 0 -> 194337 bytes
 docs/assets/webui/list-models.png                  | Bin 0 -> 203000 bytes
 docs/assets/webui/list-tabels.png                  | Bin 0 -> 293004 bytes
 docs/assets/webui/list-topics.png                  | Bin 0 -> 224115 bytes
 docs/assets/webui/model-details.png                | Bin 0 -> 184251 bytes
 docs/assets/webui/register-model.png               | Bin 0 -> 210717 bytes
 docs/assets/webui/table-details.png                | Bin 0 -> 280416 bytes
 docs/assets/webui/table-selected-details.png       | Bin 0 -> 320840 bytes
 docs/assets/webui/topic-drawer-details.png         | Bin 0 -> 256974 bytes
 docs/assets/webui/update-table-dialog.png          | Bin 0 -> 306737 bytes
 docs/assets/webui/update-topic-dialog.png          | Bin 0 -> 320990 bytes
 docs/assets/webui/version-details.png              | Bin 0 -> 180724 bytes
 docs/webui.md                                      | 182 ++++++++++-
 web/web/src/app/metalakes/metalake/MetalakeTree.js |  38 ++-
 web/web/src/app/metalakes/metalake/MetalakeView.js |  40 ++-
 .../metalake/rightContent/CreateCatalogDialog.js   | 111 ++++---
 .../metalake/rightContent/CreateFilesetDialog.js   |  18 +-
 .../metalake/rightContent/CreateSchemaDialog.js    |  18 +-
 .../metalake/rightContent/CreateTopicDialog.js     |  18 +-
 .../{CreateTopicDialog.js => LinkVersionDialog.js} | 184 +++++++----
 .../metalake/rightContent/MetalakePath.js          |  29 +-
 ...CreateTopicDialog.js => RegisterModelDialog.js} |  62 +---
 .../metalake/rightContent/RightContent.js          |  59 ++++
 .../rightContent/tabsContent/TabsContent.js        |  22 +-
 .../tabsContent/detailsView/DetailsView.js         |  19 ++
 .../tabsContent/tableView/TableView.js             |  53 +++-
 web/web/src/components/DetailsDrawer.js            |  22 +-
 web/web/src/lib/api/models/index.js                | 100 ++++++
 web/web/src/lib/store/metalakes/index.js           | 346 ++++++++++++++++++++-
 37 files changed, 1070 insertions(+), 251 deletions(-)

diff --git a/docs/assets/webui/create-table.png 
b/docs/assets/webui/create-table.png
new file mode 100644
index 000000000..4616828cf
Binary files /dev/null and b/docs/assets/webui/create-table.png differ
diff --git a/docs/assets/webui/create-topic.png 
b/docs/assets/webui/create-topic.png
new file mode 100644
index 000000000..2a5be613e
Binary files /dev/null and b/docs/assets/webui/create-topic.png differ
diff --git a/docs/assets/webui/delete-model.png 
b/docs/assets/webui/delete-model.png
new file mode 100644
index 000000000..d7614b7e7
Binary files /dev/null and b/docs/assets/webui/delete-model.png differ
diff --git a/docs/assets/webui/delete-table.png 
b/docs/assets/webui/delete-table.png
new file mode 100644
index 000000000..530c0fec5
Binary files /dev/null and b/docs/assets/webui/delete-table.png differ
diff --git a/docs/assets/webui/delete-topic.png 
b/docs/assets/webui/delete-topic.png
new file mode 100644
index 000000000..34fb5b13b
Binary files /dev/null and b/docs/assets/webui/delete-topic.png differ
diff --git a/docs/assets/webui/delete-version.png 
b/docs/assets/webui/delete-version.png
new file mode 100644
index 000000000..7eae573c3
Binary files /dev/null and b/docs/assets/webui/delete-version.png differ
diff --git a/docs/assets/webui/link-version.png 
b/docs/assets/webui/link-version.png
new file mode 100644
index 000000000..292a98c08
Binary files /dev/null and b/docs/assets/webui/link-version.png differ
diff --git a/docs/assets/webui/list-columns.png 
b/docs/assets/webui/list-columns.png
index f5235a401..f7218705c 100644
Binary files a/docs/assets/webui/list-columns.png and 
b/docs/assets/webui/list-columns.png differ
diff --git a/docs/assets/webui/list-model-versions.png 
b/docs/assets/webui/list-model-versions.png
new file mode 100644
index 000000000..2b612fd39
Binary files /dev/null and b/docs/assets/webui/list-model-versions.png differ
diff --git a/docs/assets/webui/list-models.png 
b/docs/assets/webui/list-models.png
new file mode 100644
index 000000000..753971221
Binary files /dev/null and b/docs/assets/webui/list-models.png differ
diff --git a/docs/assets/webui/list-tabels.png 
b/docs/assets/webui/list-tabels.png
new file mode 100644
index 000000000..baa0f986b
Binary files /dev/null and b/docs/assets/webui/list-tabels.png differ
diff --git a/docs/assets/webui/list-topics.png 
b/docs/assets/webui/list-topics.png
new file mode 100644
index 000000000..bb26239b2
Binary files /dev/null and b/docs/assets/webui/list-topics.png differ
diff --git a/docs/assets/webui/model-details.png 
b/docs/assets/webui/model-details.png
new file mode 100644
index 000000000..d7b7e92cd
Binary files /dev/null and b/docs/assets/webui/model-details.png differ
diff --git a/docs/assets/webui/register-model.png 
b/docs/assets/webui/register-model.png
new file mode 100644
index 000000000..949c496c2
Binary files /dev/null and b/docs/assets/webui/register-model.png differ
diff --git a/docs/assets/webui/table-details.png 
b/docs/assets/webui/table-details.png
new file mode 100644
index 000000000..0988f510d
Binary files /dev/null and b/docs/assets/webui/table-details.png differ
diff --git a/docs/assets/webui/table-selected-details.png 
b/docs/assets/webui/table-selected-details.png
new file mode 100644
index 000000000..8f35c011a
Binary files /dev/null and b/docs/assets/webui/table-selected-details.png differ
diff --git a/docs/assets/webui/topic-drawer-details.png 
b/docs/assets/webui/topic-drawer-details.png
new file mode 100644
index 000000000..b878080ea
Binary files /dev/null and b/docs/assets/webui/topic-drawer-details.png differ
diff --git a/docs/assets/webui/update-table-dialog.png 
b/docs/assets/webui/update-table-dialog.png
new file mode 100644
index 000000000..20f2b89d4
Binary files /dev/null and b/docs/assets/webui/update-table-dialog.png differ
diff --git a/docs/assets/webui/update-topic-dialog.png 
b/docs/assets/webui/update-topic-dialog.png
new file mode 100644
index 000000000..6c46ec1bf
Binary files /dev/null and b/docs/assets/webui/update-topic-dialog.png differ
diff --git a/docs/assets/webui/version-details.png 
b/docs/assets/webui/version-details.png
new file mode 100644
index 000000000..027f19acb
Binary files /dev/null and b/docs/assets/webui/version-details.png differ
diff --git a/docs/webui.md b/docs/webui.md
index 960eb821f..cf079d3f1 100644
--- a/docs/webui.md
+++ b/docs/webui.md
@@ -160,11 +160,12 @@ Click on the `CREATE CATALOG` button displays the dialog 
to create a catalog.
 Creating a catalog requires these fields:
 
 1. **Catalog name**(**_required_**): the name of the catalog
-2. **Type**(**_required_**): `relational`/`fileset`/`messaging`, the default 
value is `relational`
+2. **Type**(**_required_**): `relational`/`fileset`/`messaging`/`model`, the 
default value is `relational`
 3. **Provider**(**_required_**):
     1. Type `relational` - 
`hive`/`iceberg`/`mysql`/`postgresql`/`doris`/`paimon`/`hudi`/`oceanbase`
     2. Type `fileset` - `hadoop`
     3. Type `messaging` - `kafka`
+    4. Type `model` has no provider
 4. **Comment**(_optional_): the comment of this catalog
 5. **Properties**(**each `provider` must fill in the required property fields 
specifically**)
 
@@ -425,6 +426,7 @@ Displays a confirmation dialog, clicking on the SUBMIT 
button deletes this catal
 ![delete-catalog](./assets/webui/delete-catalog.png)
 
 ### Schema
+
 Click the catalog tree node on the left sidebar or the catalog name link in 
the table cell.
 
 Displays the list schemas of the catalog.
@@ -469,14 +471,63 @@ Displays a confirmation dialog, clicking on the `DROP` 
button drops this schema.
 
 ### Table
 
-![list-tables](./assets/webui/list-tables.png)
+Click the hive schema tree node on the left sidebar or the schema name link in 
the table cell.
+
+Displays the list tables of the schema.
+
+![list-tables](./assets/webui/list-tabels.png)
+
+#### Create table
+
+Click on the `CREATE TABLE` button displays the dialog to create a table.
+
+![create-table](./assets/webui/create-table.png)
+
+Creating a table needs these fields:
+
+1. **Name**(**_required_**): the name of the table.
+2. **columns**(**_required_**): 
+    1. The name and type of each column are required.
+    2. Only suppport simple types, cannot support complex types by ui, you can 
create complex types by api.
+3. **Comment**(_optional_): the comment of the table.
+4. **Properties**(_optional_): Click on the `ADD PROPERTY` button to add 
custom properties.
+
+#### Show table details
+
+Click on the action icon <Icon icon='bx:show-alt' fontSize='24' /> in the 
table cell.
+
+You can see the detailed information of this table in the drawer component on 
the right.
+
+![table-details](./assets/webui/table-details.png)
+
+Click the table tree node on the left sidebar or the table name link in the 
table cell.
+
+You can see the columns and detailed information on the right page.
 
 ![list-columns](./assets/webui/list-columns.png)
+![table-selected-details](./assets/webui/table-selected-details.png)
+
+#### Edit table
+
+Click on the action icon <Icon icon='mdi:square-edit-outline' fontSize='24' /> 
in the table cell.
+
+Displays the dialog for modifying fields of the selected table.
+
+![update-table-dialog](./assets/webui/update-table-dialog.png)
+
+#### Drop table
+
+Click on the action icon <Icon icon='mdi:delete-outline' fontSize='24' 
color='red' /> in the table cell.
+
+Displays a confirmation dialog, clicking on the `DROP` button drops this table.
+
+![delete-table](./assets/webui/delete-table.png)
 
 ### Fileset
+
 Click the fileset schema tree node on the left sidebar or the schema name link 
in the table cell.
 
-Displays the list fileset of the schema.
+Displays the list filesets of the schema.
 
 ![list-filesets](./assets/webui/list-filesets.png)
 
@@ -528,8 +579,127 @@ Displays a confirmation dialog, clicking on the `DROP` 
button drops this fileset
 
 ### Topic
 
+Click the kafka schema tree node on the left sidebar or the schema name link 
in the table cell.
+
+Displays the list topics of the schema.
+
+![list-topics](./assets/webui/list-topics.png)
+
+#### Create topic
+
+Click on the `CREATE TOPIC` button displays the dialog to create a topic.
+
+![create-topic](./assets/webui/create-topic.png)
+
+Creating a topic needs these fields:
+
+1. **Name**(**_required_**): the name of the topic.
+2. **Comment**(_optional_): the comment of the topic.
+3. **Properties**(_optional_): Click on the `ADD PROPERTY` button to add 
custom properties.
+
+#### Show topic details
+
+Click on the action icon <Icon icon='bx:show-alt' fontSize='24' /> in the 
table cell.
+
+You can see the detailed information of this topic in the drawer component on 
the right.
+
+![topic-details](./assets/webui/topic-drawer-details.png)
+
+Click the topic tree node on the left sidebar or the topic name link in the 
table cell.
+
+You can see the detailed information on the right page.
+
 ![topic-details](./assets/webui/topic-details.png)
 
+#### Edit topic
+
+Click on the action icon <Icon icon='mdi:square-edit-outline' fontSize='24' /> 
in the table cell.
+
+Displays the dialog for modifying fields of the selected topic.
+
+![update-topic-dialog](./assets/webui/update-topic-dialog.png)
+
+#### Drop topic
+
+Click on the action icon <Icon icon='mdi:delete-outline' fontSize='24' 
color='red' /> in the table cell.
+
+Displays a confirmation dialog, clicking on the `DROP` button drops this topic.
+
+![delete-topic](./assets/webui/delete-topic.png)
+
+### Model
+
+Click the model schema tree node on the left sidebar or the schema name link 
in the table cell.
+
+Displays the list model of the schema.
+
+![list-models](./assets/webui/list-models.png)
+
+#### Register model
+
+Click on the `REGISTER MODEL` button displays the dialog to register a model.
+
+![register-model](./assets/webui/register-model.png)
+
+Register a model needs these fields:
+
+1. **Name**(**_required_**): the name of the model.
+2. **Comment**(_optional_): the comment of the model.
+3. **Properties**(_optional_): Click on the `ADD PROPERTY` button to add 
custom properties.
+
+#### Show model details
+
+Click on the action icon <Icon icon='bx:show-alt' fontSize='24' /> in the 
table cell.
+
+You can see the detailed information of this model in the drawer component on 
the right.
+
+![model-details](./assets/webui/model-details.png)
+
+#### Drop model
+
+Click on the action icon <Icon icon='mdi:delete-outline' fontSize='24' 
color='red' /> in the table cell.
+
+Displays a confirmation dialog, clicking on the `DROP` button drops this model.
+
+![delete-model](./assets/webui/delete-model.png)
+
+### Version
+
+Click the model tree node on the left sidebar or the model name link in the 
table cell.
+
+Displays the list versions of the model.
+
+![list-model-versions](./assets/webui/list-model-versions.png)
+
+#### Link version
+
+Click on the `LINK VERSION` button displays the dialog to link a version.
+
+![link-version](./assets/webui/link-version.png)
+
+Link a version needs these fields:
+
+1. **URI**(**_required_**): the uri of the version.
+2. **Aliases**(**_required_**): the aliases of the version, aliase cannot be 
number or number string.
+3. **Comment**(_optional_): the comment of the model.
+4. **Properties**(_optional_): Click on the `ADD PROPERTY` button to add 
custom properties.
+
+#### Show version details
+
+Click on the action icon <Icon icon='bx:show-alt' fontSize='24' /> in the 
table cell.
+
+You can see the detailed information of this version in the drawer component 
on the right.
+
+![version-details](./assets/webui/version-details.png)
+
+#### Drop version
+
+Click on the action icon <Icon icon='mdi:delete-outline' fontSize='24' 
color='red' /> in the table cell.
+
+Displays a confirmation dialog, clicking on the `DROP` button drops this 
version.
+
+![delete-version](./assets/webui/delete-version.png)
+
 ## Feature capabilities
 
 | Page     | Capabilities                                                      
                |
@@ -537,9 +707,11 @@ Displays a confirmation dialog, clicking on the `DROP` 
button drops this fileset
 | Metalake | _`View`_ &#10004; / _`Create`_ &#10004; / _`Edit`_ &#10004; / 
_`Delete`_ &#10004; |
 | Catalog  | _`View`_ &#10004; / _`Create`_ &#10004; / _`Edit`_ &#10004; / 
_`Delete`_ &#10004; |
 | Schema   | _`View`_ &#10004; / _`Create`_ &#10004; / _`Edit`_ &#10004; / 
_`Delete`_ &#10004; |
-| Table    | _`View`_ &#10004; / _`Create`_ &#10008; / _`Edit`_ &#10008; / 
_`Delete`_ &#10008; |
+| Table    | _`View`_ &#10004; / _`Create`_ &#10004; / _`Edit`_ &#10004; / 
_`Delete`_ &#10004; |
 | Fileset  | _`View`_ &#10004; / _`Create`_ &#10004; / _`Edit`_ &#10004; / 
_`Delete`_ &#10004; |
-| Topic    | _`View`_ &#10004; / _`Create`_ &#10008; / _`Edit`_ &#10008; / 
_`Delete`_ &#10008; |
+| Topic    | _`View`_ &#10004; / _`Create`_ &#10004; / _`Edit`_ &#10004; / 
_`Delete`_ &#10004; |
+| Model    | _`View`_ &#10004; / _`Create`_ &#10004; / _`Edit`_ &#10008; / 
_`Delete`_ &#10004; |
+| Version  | _`View`_ &#10004; / _`Create`_ &#10004; / _`Edit`_ &#10008; / 
_`Delete`_ &#10004; |
 
 ## E2E test
 
diff --git a/web/web/src/app/metalakes/metalake/MetalakeTree.js 
b/web/web/src/app/metalakes/metalake/MetalakeTree.js
index e6b6ea0c3..dead6c338 100644
--- a/web/web/src/app/metalakes/metalake/MetalakeTree.js
+++ b/web/web/src/app/metalakes/metalake/MetalakeTree.js
@@ -38,7 +38,10 @@ import {
   setLoadedNodes,
   getTableDetails,
   getFilesetDetails,
-  getTopicDetails
+  getTopicDetails,
+  getModelDetails,
+  fetchModelVersions,
+  getVersionDetails
 } from '@/lib/store/metalakes'
 
 import { extractPlaceholder } from '@/lib/utils'
@@ -81,6 +84,8 @@ const MetalakeTree = props => {
         return 'skill-icons:kafka'
       case 'fileset':
         return 'twemoji:file-folder'
+      case 'model':
+        return 'carbon:machine-learning-model'
       default:
         return 'bx:book'
     }
@@ -115,6 +120,23 @@ const MetalakeTree = props => {
         }
         break
       }
+      case 'model': {
+        if (store.selectedNodes.includes(nodeProps.data.key)) {
+          const pathArr = extractPlaceholder(nodeProps.data.key)
+          const [metalake, catalog, type, schema, model] = pathArr
+          dispatch(fetchModelVersions({ init: true, metalake, catalog, schema, 
model }))
+          dispatch(getModelDetails({ init: true, metalake, catalog, schema, 
model }))
+        }
+        break
+      }
+      case 'version': {
+        if (store.selectedNodes.includes(nodeProps.data.key)) {
+          const pathArr = extractPlaceholder(nodeProps.data.key)
+          const [metalake, catalog, type, schema, model, version] = pathArr
+          dispatch(getVersionDetails({ init: true, metalake, catalog, schema, 
model, version }))
+        }
+        break
+      }
       default:
         dispatch(setIntoTreeNodeWithFetch({ key: nodeProps.data.key, reload: 
true }))
     }
@@ -257,6 +279,20 @@ const MetalakeTree = props => {
           </IconButton>
         )
 
+      case 'model':
+        return (
+          <IconButton
+            disableRipple={!store.selectedNodes.includes(nodeProps.data.key)}
+            size='small'
+            sx={{ color: '#666' }}
+            onClick={e => handleClickIcon(e, nodeProps)}
+            onMouseEnter={e => onMouseEnter(e, nodeProps)}
+            onMouseLeave={e => onMouseLeave(e, nodeProps)}
+          >
+            <Icon icon={isHover !== nodeProps.data.key ? 'mdi:globe-model' : 
'mdi:reload'} fontSize='inherit' />
+          </IconButton>
+        )
+
       default:
         return <></>
     }
diff --git a/web/web/src/app/metalakes/metalake/MetalakeView.js 
b/web/web/src/app/metalakes/metalake/MetalakeView.js
index a7726bea5..58c16f9e6 100644
--- a/web/web/src/app/metalakes/metalake/MetalakeView.js
+++ b/web/web/src/app/metalakes/metalake/MetalakeView.js
@@ -34,12 +34,16 @@ import {
   fetchTables,
   fetchFilesets,
   fetchTopics,
+  fetchModels,
+  fetchModelVersions,
   getMetalakeDetails,
   getCatalogDetails,
   getSchemaDetails,
   getTableDetails,
   getFilesetDetails,
   getTopicDetails,
+  getModelDetails,
+  getVersionDetails,
   setSelectedNodes
 } from '@/lib/store/metalakes'
 
@@ -49,6 +53,12 @@ const MetalakeView = () => {
   const paramsSize = [...searchParams.keys()].length
   const store = useAppSelector(state => state.metalakes)
 
+  const buildNodePath = routeParams => {
+    const keys = ['metalake', 'catalog', 'type', 'schema', 'table', 'fileset', 
'topic', 'model']
+
+    return keys.map(key => (routeParams[key] ? `{{${routeParams[key]}}}` : 
'')).join('')
+  }
+
   useEffect(() => {
     const routeParams = {
       metalake: searchParams.get('metalake'),
@@ -57,11 +67,13 @@ const MetalakeView = () => {
       schema: searchParams.get('schema'),
       table: searchParams.get('table'),
       fileset: searchParams.get('fileset'),
-      topic: searchParams.get('topic')
+      topic: searchParams.get('topic'),
+      model: searchParams.get('model'),
+      version: searchParams.get('version')
     }
     async function fetchDependsData() {
       if ([...searchParams.keys()].length) {
-        const { metalake, catalog, type, schema, table, fileset, topic } = 
routeParams
+        const { metalake, catalog, type, schema, table, fileset, topic, model, 
version } = routeParams
 
         if (paramsSize === 1 && metalake) {
           dispatch(fetchCatalogs({ init: true, page: 'metalakes', metalake }))
@@ -91,6 +103,9 @@ const MetalakeView = () => {
             case 'messaging':
               dispatch(fetchTopics({ init: true, page: 'schemas', metalake, 
catalog, schema }))
               break
+            case 'model':
+              dispatch(fetchModels({ init: true, page: 'schemas', metalake, 
catalog, schema }))
+              break
             default:
               break
           }
@@ -111,24 +126,19 @@ const MetalakeView = () => {
           if (topic) {
             dispatch(getTopicDetails({ init: true, metalake, catalog, schema, 
topic }))
           }
+          if (model) {
+            dispatch(fetchModelVersions({ init: true, metalake, catalog, 
schema, model }))
+            dispatch(getModelDetails({ init: true, metalake, catalog, schema, 
model }))
+          }
+        }
+        if (paramsSize === 6 && version) {
+          dispatch(getVersionDetails({ init: true, metalake, catalog, schema, 
model, version }))
         }
       }
     }
     fetchDependsData()
 
-    dispatch(
-      setSelectedNodes(
-        routeParams.catalog
-          ? [
-              
`{{${routeParams.metalake}}}{{${routeParams.catalog}}}{{${routeParams.type}}}${
-                routeParams.schema ? `{{${routeParams.schema}}}` : ''
-              }${routeParams.table ? `{{${routeParams.table}}}` : ''}${
-                routeParams.fileset ? `{{${routeParams.fileset}}}` : ''
-              }${routeParams.topic ? `{{${routeParams.topic}}}` : ''}`
-            ]
-          : []
-      )
-    )
+    dispatch(setSelectedNodes(routeParams.catalog ? 
[buildNodePath(routeParams)] : []))
 
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [searchParams])
diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js 
b/web/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js
index cd9c101f7..6f0cf70ed 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateCatalogDialog.js
@@ -64,7 +64,7 @@ const defaultValues = {
 
 const schema = yup.object().shape({
   name: yup.string().required().matches(nameRegex, nameRegexDesc),
-  type: yup.mixed().oneOf(['relational', 'fileset', 'messaging']).required(),
+  type: yup.mixed().oneOf(['relational', 'fileset', 'messaging', 
'model']).required(),
   provider: yup.string().when('type', (type, schema) => {
     switch (type) {
       case 'relational':
@@ -148,12 +148,7 @@ const CreateCatalogDialog = props => {
   }
 
   const addFields = () => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
 
     if (duplicateKeys) {
       return
@@ -212,16 +207,9 @@ const CreateCatalogDialog = props => {
   }
 
   const onSubmit = data => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const hasError = innerProps.some(prop => prop.hasDuplicateKey || 
prop.invalid)
 
-    const invalidKeys = innerProps.some(i => i.invalid)
-
-    if (duplicateKeys || invalidKeys) {
+    if (hasError) {
       return
     }
 
@@ -371,6 +359,11 @@ const CreateCatalogDialog = props => {
         setValue('provider', 'kafka')
         break
       }
+      case 'model': {
+        setProviderTypes([])
+        setValue('provider', '')
+        break
+      }
     }
 
     // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -385,11 +378,10 @@ const CreateCatalogDialog = props => {
       defaultProps = providerTypes[providerItemIndex].defaultProps
 
       resetPropsFields(providerTypes, providerItemIndex)
-
-      if (type === 'create') {
-        setInnerProps(defaultProps)
-        setValue('propItems', providerTypes[providerItemIndex].defaultProps)
-      }
+    }
+    if (type === 'create') {
+      setInnerProps(defaultProps)
+      setValue('propItems', defaultProps)
     }
 
     // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -420,12 +412,16 @@ const CreateCatalogDialog = props => {
           providersItems = messagingProviders
           break
         }
+        case 'model': {
+          providersItems = []
+          break
+        }
       }
 
       setProviderTypes(providersItems)
 
       const providerItem = providersItems.find(i => i.value === data.provider)
-      let propsItems = [...providerItem.defaultProps].filter(i => i.required)
+      let propsItems = providerItem ? [...providerItem.defaultProps].filter(i 
=> i.required) : []
 
       propsItems = propsItems.map((it, idx) => {
         let propItem = {
@@ -528,6 +524,7 @@ const CreateCatalogDialog = props => {
                       <MenuItem value={'relational'}>Relational</MenuItem>
                       <MenuItem value={'fileset'}>Fileset</MenuItem>
                       <MenuItem value={'messaging'}>Messaging</MenuItem>
+                      <MenuItem value={'model'}>Model</MenuItem>
                     </Select>
                   )}
                 />
@@ -535,41 +532,43 @@ const CreateCatalogDialog = props => {
               </FormControl>
             </Grid>
 
-            <Grid item xs={12}>
-              <FormControl fullWidth>
-                <InputLabel id='select-catalog-provider' 
error={Boolean(errors.provider)}>
-                  Provider
-                </InputLabel>
-                <Controller
-                  name='provider'
-                  control={control}
-                  rules={{ required: true }}
-                  render={({ field: { value, onChange } }) => (
-                    <Select
-                      value={value}
-                      label='Provider'
-                      defaultValue='hive'
-                      onChange={e => handleChangeProvider(onChange, e)}
-                      error={Boolean(errors.provider)}
-                      labelId='select-catalog-provider'
-                      disabled={type === 'update'}
-                      data-refer='catalog-provider-selector'
-                    >
-                      {providerTypes.map(item => {
-                        return (
-                          <MenuItem key={item.label} value={item.value}>
-                            {item.label}
-                          </MenuItem>
-                        )
-                      })}
-                    </Select>
+            {typeSelect !== 'model' && (
+              <Grid item xs={12}>
+                <FormControl fullWidth>
+                  <InputLabel id='select-catalog-provider' 
error={Boolean(errors.provider)}>
+                    Provider
+                  </InputLabel>
+                  <Controller
+                    name='provider'
+                    control={control}
+                    rules={{ required: true }}
+                    render={({ field: { value, onChange } }) => (
+                      <Select
+                        value={value}
+                        label='Provider'
+                        defaultValue='hive'
+                        onChange={e => handleChangeProvider(onChange, e)}
+                        error={Boolean(errors.provider)}
+                        labelId='select-catalog-provider'
+                        disabled={type === 'update'}
+                        data-refer='catalog-provider-selector'
+                      >
+                        {providerTypes.map(item => {
+                          return (
+                            <MenuItem key={item.label} value={item.value}>
+                              {item.label}
+                            </MenuItem>
+                          )
+                        })}
+                      </Select>
+                    )}
+                  />
+                  {errors.provider && (
+                    <FormHelperText sx={{ color: 'error.main' 
}}>{errors.provider.message}</FormHelperText>
                   )}
-                />
-                {errors.provider && (
-                  <FormHelperText sx={{ color: 'error.main' 
}}>{errors.provider.message}</FormHelperText>
-                )}
-              </FormControl>
-            </Grid>
+                </FormControl>
+              </Grid>
+            )}
 
             <Grid item xs={12}>
               <FormControl fullWidth>
diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js 
b/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js
index 6a69d82f8..873876e6e 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateFilesetDialog.js
@@ -135,12 +135,7 @@ const CreateFilesetDialog = props => {
   }
 
   const addFields = () => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
 
     if (duplicateKeys) {
       return
@@ -173,16 +168,9 @@ const CreateFilesetDialog = props => {
   }
 
   const onSubmit = data => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const hasError = innerProps.some(prop => prop.hasDuplicateKey || 
prop.invalid)
 
-    const invalidKeys = innerProps.some(i => i.invalid)
-
-    if (duplicateKeys || invalidKeys) {
+    if (hasError) {
       return
     }
 
diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js 
b/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js
index ce893802a..d09b9052b 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateSchemaDialog.js
@@ -131,12 +131,7 @@ const CreateSchemaDialog = props => {
   }
 
   const addFields = () => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
 
     if (duplicateKeys) {
       return
@@ -169,16 +164,9 @@ const CreateSchemaDialog = props => {
   }
 
   const onSubmit = data => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const hasError = innerProps.some(prop => prop.hasDuplicateKey || 
prop.invalid)
 
-    const invalidKeys = innerProps.some(i => i.invalid)
-
-    if (duplicateKeys || invalidKeys) {
+    if (hasError) {
       return
     }
 
diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js 
b/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
index 4f671cc87..87d33cef7 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
@@ -127,12 +127,7 @@ const CreateTopicDialog = props => {
   }
 
   const addFields = () => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
 
     if (duplicateKeys) {
       return
@@ -165,16 +160,9 @@ const CreateTopicDialog = props => {
   }
 
   const onSubmit = data => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const hasError = innerProps.some(prop => prop.hasDuplicateKey || 
prop.invalid)
 
-    const invalidKeys = innerProps.some(i => i.invalid)
-
-    if (duplicateKeys || invalidKeys) {
+    if (hasError) {
       return
     }
 
diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js 
b/web/web/src/app/metalakes/metalake/rightContent/LinkVersionDialog.js
similarity index 70%
copy from web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
copy to web/web/src/app/metalakes/metalake/rightContent/LinkVersionDialog.js
index 4f671cc87..01ea35b0c 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/LinkVersionDialog.js
@@ -40,26 +40,53 @@ import {
 import Icon from '@/components/Icon'
 
 import { useAppDispatch } from '@/lib/hooks/useStore'
-import { createTopic, updateTopic } from '@/lib/store/metalakes'
+import { linkVersion } from '@/lib/store/metalakes'
 
 import * as yup from 'yup'
-import { useForm, Controller } from 'react-hook-form'
+import { useForm, Controller, useFieldArray } from 'react-hook-form'
 import { yupResolver } from '@hookform/resolvers/yup'
 
 import { groupBy } from 'lodash-es'
-import { genUpdates } from '@/lib/utils'
-import { nameRegex, nameRegexDesc, keyRegex } from '@/lib/utils/regex'
+import { keyRegex } from '@/lib/utils/regex'
 import { useSearchParams } from 'next/navigation'
 import { useAppSelector } from '@/lib/hooks/useStore'
 
 const defaultValues = {
-  name: '',
+  uri: '',
+  aliases: [{ name: '' }],
   comment: '',
   propItems: []
 }
 
 const schema = yup.object().shape({
-  name: yup.string().required().matches(nameRegex, nameRegexDesc),
+  uri: yup.string().required(),
+  aliases: yup
+    .array()
+    .of(
+      yup.object().shape({
+        name: yup
+          .string()
+          .required('This aliase is required')
+          .test('not-number', 'Aliase cannot be a number or numeric string', 
value => {
+            return value === undefined || isNaN(Number(value))
+          })
+      })
+    )
+    .test('unique', 'Aliase must be unique', (aliases, ctx) => {
+      const values = aliases?.filter(a => !!a.name).map(a => a.name)
+      const duplicates = values.filter((value, index, self) => 
self.indexOf(value) !== index)
+
+      if (duplicates.length > 0) {
+        const duplicateIndex = values.lastIndexOf(duplicates[0])
+
+        return ctx.createError({
+          path: `aliases.${duplicateIndex}.name`,
+          message: 'This aliase is duplicated'
+        })
+      }
+
+      return true
+    }),
   propItems: yup.array().of(
     yup.object().shape({
       required: yup.boolean(),
@@ -76,13 +103,14 @@ const Transition = forwardRef(function Transition(props, 
ref) {
   return <Fade ref={ref} {...props} />
 })
 
-const CreateTopicDialog = props => {
+const LinkVersionDialog = props => {
   const { open, setOpen, type = 'create', data = {} } = props
   const searchParams = useSearchParams()
   const metalake = searchParams.get('metalake')
   const catalog = searchParams.get('catalog')
   const schemaName = searchParams.get('schema')
   const catalogType = searchParams.get('type')
+  const model = searchParams.get('model')
   const [innerProps, setInnerProps] = useState([])
   const dispatch = useAppDispatch()
   const store = useAppSelector(state => state.metalakes)
@@ -127,12 +155,7 @@ const CreateTopicDialog = props => {
   }
 
   const addFields = () => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
 
     if (duplicateKeys) {
       return
@@ -151,6 +174,13 @@ const CreateTopicDialog = props => {
     setValue('propItems', data)
   }
 
+  const { fields, append, remove } = useFieldArray({
+    control,
+    name: 'aliases'
+  })
+
+  const watchAliases = watch('aliases')
+
   const handleClose = () => {
     reset()
     setInnerProps([])
@@ -165,16 +195,9 @@ const CreateTopicDialog = props => {
   }
 
   const onSubmit = data => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const hasError = innerProps.some(prop => prop.hasDuplicateKey || 
prop.invalid)
 
-    const invalidKeys = innerProps.some(i => i.invalid)
-
-    if (duplicateKeys || invalidKeys) {
+    if (hasError) {
       return
     }
 
@@ -190,38 +213,20 @@ const CreateTopicDialog = props => {
         }, {})
 
         const schemaData = {
-          name: data.name,
+          uri: data.uri,
+          aliases: data.aliases.map(alias => alias.name),
           comment: data.comment,
           properties
         }
 
         if (type === 'create') {
-          dispatch(createTopic({ data: schemaData, metalake, catalog, schema: 
schemaName, type: catalogType })).then(
-            res => {
-              if (!res.payload?.err) {
-                handleClose()
-              }
+          dispatch(
+            linkVersion({ data: schemaData, metalake, catalog, schema: 
schemaName, type: catalogType, model })
+          ).then(res => {
+            if (!res.payload?.err) {
+              handleClose()
             }
-          )
-        } else {
-          const reqData = { updates: genUpdates(cacheData, schemaData) }
-
-          if (reqData.updates.length !== 0) {
-            dispatch(
-              updateTopic({
-                metalake,
-                catalog,
-                type: catalogType,
-                schema: schemaName,
-                topic: cacheData.name,
-                data: reqData
-              })
-            ).then(res => {
-              if (!res.payload?.err) {
-                handleClose()
-              }
-            })
-          }
+          })
         }
       })
       .catch(err => {
@@ -238,7 +243,7 @@ const CreateTopicDialog = props => {
       const { properties = {} } = data
 
       setCacheData(data)
-      setValue('name', data.name)
+      setValue('uri', data.uri)
       setValue('comment', data.comment)
 
       const propsItems = Object.entries(properties).map(([key, value]) => {
@@ -273,7 +278,7 @@ const CreateTopicDialog = props => {
           </IconButton>
           <Box sx={{ mb: 8, textAlign: 'center' }}>
             <Typography variant='h5' sx={{ mb: 3 }}>
-              {type === 'create' ? 'Create' : 'Edit'} Topic
+              {type === 'create' ? 'Link' : 'Edit'} Version
             </Typography>
           </Box>
 
@@ -281,25 +286,76 @@ const CreateTopicDialog = props => {
             <Grid item xs={12}>
               <FormControl fullWidth>
                 <Controller
-                  name='name'
+                  name='uri'
                   control={control}
                   rules={{ required: true }}
                   render={({ field: { value, onChange } }) => (
                     <TextField
                       value={value}
-                      label='Name'
+                      label='URI'
                       onChange={onChange}
                       placeholder=''
                       disabled={type === 'update'}
-                      error={Boolean(errors.name)}
-                      data-refer='topic-name-field'
+                      error={Boolean(errors.uri)}
+                      data-refer='link-uri-field'
                     />
                   )}
                 />
-                {errors.name && <FormHelperText sx={{ color: 'error.main' 
}}>{errors.name.message}</FormHelperText>}
+                {errors.uri && <FormHelperText sx={{ color: 'error.main' 
}}>{errors.uri.message}</FormHelperText>}
               </FormControl>
             </Grid>
 
+            <Grid item xs={12}>
+              {fields.map((field, index) => {
+                return (
+                  <Grid key={index} item xs={12} sx={{ '& + &': { mt: 2 } }}>
+                    <FormControl fullWidth>
+                      <Box
+                        key={field.id}
+                        sx={{ display: 'flex', alignItems: 'center', 
justifyContent: 'space-between' }}
+                        data-refer={`version-aliases-${index}`}
+                      >
+                        <Box sx={{ flexGrow: 1 }}>
+                          <Controller
+                            name={`aliases.${index}.name`}
+                            control={control}
+                            render={({ field }) => (
+                              <TextField
+                                {...field}
+                                onChange={event => {
+                                  field.onChange(event)
+                                  trigger('aliases')
+                                }}
+                                label={`Aliase ${index + 1}`}
+                                error={!!errors.aliases?.[index]?.name || 
!!errors.aliases?.message}
+                                
helperText={errors.aliases?.[index]?.name?.message || errors.aliases?.message}
+                                fullWidth
+                              />
+                            )}
+                          />
+                        </Box>
+                        <Box>
+                          {index === 0 ? (
+                            <Box sx={{ minWidth: 40 }}>
+                              <IconButton onClick={() => append({ name: '' })}>
+                                <Icon icon='mdi:plus-circle-outline' />
+                              </IconButton>
+                            </Box>
+                          ) : (
+                            <Box sx={{ minWidth: 40 }}>
+                              <IconButton onClick={() => remove(index)}>
+                                <Icon icon='mdi:minus-circle-outline' />
+                              </IconButton>
+                            </Box>
+                          )}
+                        </Box>
+                      </Box>
+                    </FormControl>
+                  </Grid>
+                )
+              })}
+            </Grid>
+
             <Grid item xs={12}>
               <FormControl fullWidth>
                 <Controller
@@ -315,14 +371,14 @@ const CreateTopicDialog = props => {
                       onChange={onChange}
                       placeholder=''
                       error={Boolean(errors.comment)}
-                      data-refer='topic-comment-field'
+                      data-refer='version-comment-field'
                     />
                   )}
                 />
               </FormControl>
             </Grid>
 
-            <Grid item xs={12} data-refer='topic-props-layout'>
+            <Grid item xs={12} data-refer='version-props-layout'>
               <Typography sx={{ mb: 2 }} variant='body2'>
                 Properties
               </Typography>
@@ -334,7 +390,7 @@ const CreateTopicDialog = props => {
                         <Box>
                           <Box
                             sx={{ display: 'flex', alignItems: 'center', 
justifyContent: 'space-between' }}
-                            data-refer={`topic-props-${index}`}
+                            data-refer={`version-props-${index}`}
                           >
                             <Box>
                               <TextField
@@ -344,7 +400,7 @@ const CreateTopicDialog = props => {
                                 value={item.key}
                                 disabled={item.disabled || (item.key === 
'location' && type === 'update')}
                                 onChange={event => handleFormChange({ index, 
event })}
-                                error={item.hasDuplicateKey || item.invalid || 
!item.key.trim()}
+                                error={item.hasDuplicateKey || item.invalid || 
!item.key?.trim()}
                                 data-refer={`props-key-${index}`}
                               />
                             </Box>
@@ -390,7 +446,7 @@ const CreateTopicDialog = props => {
                             underscores, hyphens, or dots.
                           </FormHelperText>
                         )}
-                        {!item.key.trim() && (
+                        {!item.key?.trim() && (
                           <FormHelperText 
className={'twc-text-error-main'}>Key is required</FormHelperText>
                         )}
                       </FormControl>
@@ -406,7 +462,7 @@ const CreateTopicDialog = props => {
                 onClick={addFields}
                 variant='outlined'
                 startIcon={<Icon icon='mdi:plus-circle-outline' />}
-                data-refer='add-topic-props'
+                data-refer='add-version-props'
               >
                 Add Property
               </Button>
@@ -420,8 +476,8 @@ const CreateTopicDialog = props => {
             pb: theme => [`${theme.spacing(5)} !important`, 
`${theme.spacing(12.5)} !important`]
           }}
         >
-          <Button variant='contained' sx={{ mr: 1 }} type='submit' 
data-refer='handle-submit-topic'>
-            {type === 'create' ? 'Create' : 'Update'}
+          <Button variant='contained' sx={{ mr: 1 }} type='submit' 
data-refer='handle-submit-model'>
+            {type === 'create' ? 'Submit' : 'Update'}
           </Button>
           <Button variant='outlined' onClick={handleClose}>
             Cancel
@@ -432,4 +488,4 @@ const CreateTopicDialog = props => {
   )
 }
 
-export default CreateTopicDialog
+export default LinkVersionDialog
diff --git a/web/web/src/app/metalakes/metalake/rightContent/MetalakePath.js 
b/web/web/src/app/metalakes/metalake/rightContent/MetalakePath.js
index b991ed2f1..9f41ccc0e 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/MetalakePath.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/MetalakePath.js
@@ -47,10 +47,12 @@ const MetalakePath = props => {
     schema: searchParams.get('schema'),
     table: searchParams.get('table'),
     fileset: searchParams.get('fileset'),
-    topic: searchParams.get('topic')
+    topic: searchParams.get('topic'),
+    model: searchParams.get('model'),
+    version: searchParams.get('version')
   }
 
-  const { metalake, catalog, type, schema, table, fileset, topic } = 
routeParams
+  const { metalake, catalog, type, schema, table, fileset, topic, model, 
version } = routeParams
 
   const metalakeUrl = `?metalake=${metalake}`
   const catalogUrl = `?metalake=${metalake}&catalog=${catalog}&type=${type}`
@@ -58,6 +60,8 @@ const MetalakePath = props => {
   const tableUrl = 
`?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}&table=${table}`
   const filesetUrl = 
`?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}&fileset=${fileset}`
   const topicUrl = 
`?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}&topic=${topic}`
+  const modelUrl = 
`?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}&model=${model}`
+  const versionUrl = 
`?metalake=${metalake}&catalog=${catalog}&type=${type}&schema=${schema}&model=${model}&version=${version}`
 
   const handleClick = (event, path) => {
     path === `?${searchParams.toString()}` && event.preventDefault()
@@ -152,6 +156,27 @@ const MetalakePath = props => {
           </MUILink>
         </Tooltip>
       )}
+      {model && (
+        <Tooltip title={model} placement='top'>
+          <MUILink component={Link} href={modelUrl} onClick={event => 
handleClick(event, modelUrl)} underline='hover'>
+            <Icon icon='bx:file' fontSize={20} />
+            <Text>{model}</Text>
+          </MUILink>
+        </Tooltip>
+      )}
+      {version && (
+        <Tooltip title={version} placement='top'>
+          <MUILink
+            component={Link}
+            href={versionUrl}
+            onClick={event => handleClick(event, versionUrl)}
+            underline='hover'
+          >
+            <Icon icon='bx:file' fontSize={20} />
+            <Text>{version}</Text>
+          </MUILink>
+        </Tooltip>
+      )}
     </Breadcrumbs>
   )
 }
diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js 
b/web/web/src/app/metalakes/metalake/rightContent/RegisterModelDialog.js
similarity index 86%
copy from web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
copy to web/web/src/app/metalakes/metalake/rightContent/RegisterModelDialog.js
index 4f671cc87..68661fa9b 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/CreateTopicDialog.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/RegisterModelDialog.js
@@ -40,14 +40,13 @@ import {
 import Icon from '@/components/Icon'
 
 import { useAppDispatch } from '@/lib/hooks/useStore'
-import { createTopic, updateTopic } from '@/lib/store/metalakes'
+import { registerModel } from '@/lib/store/metalakes'
 
 import * as yup from 'yup'
 import { useForm, Controller } from 'react-hook-form'
 import { yupResolver } from '@hookform/resolvers/yup'
 
 import { groupBy } from 'lodash-es'
-import { genUpdates } from '@/lib/utils'
 import { nameRegex, nameRegexDesc, keyRegex } from '@/lib/utils/regex'
 import { useSearchParams } from 'next/navigation'
 import { useAppSelector } from '@/lib/hooks/useStore'
@@ -76,7 +75,7 @@ const Transition = forwardRef(function Transition(props, ref) 
{
   return <Fade ref={ref} {...props} />
 })
 
-const CreateTopicDialog = props => {
+const RegisterModelDialog = props => {
   const { open, setOpen, type = 'create', data = {} } = props
   const searchParams = useSearchParams()
   const metalake = searchParams.get('metalake')
@@ -127,12 +126,7 @@ const CreateTopicDialog = props => {
   }
 
   const addFields = () => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const duplicateKeys = innerProps.some(item => item.hasDuplicateKey)
 
     if (duplicateKeys) {
       return
@@ -165,16 +159,9 @@ const CreateTopicDialog = props => {
   }
 
   const onSubmit = data => {
-    const duplicateKeys = innerProps
-      .filter(item => item.key.trim() !== '')
-      .some(
-        (item, index, filteredItems) =>
-          filteredItems.findIndex(otherItem => otherItem !== item && 
otherItem.key.trim() === item.key.trim()) !== -1
-      )
+    const hasError = innerProps.some(prop => prop.hasDuplicateKey || 
prop.invalid)
 
-    const invalidKeys = innerProps.some(i => i.invalid)
-
-    if (duplicateKeys || invalidKeys) {
+    if (hasError) {
       return
     }
 
@@ -196,32 +183,13 @@ const CreateTopicDialog = props => {
         }
 
         if (type === 'create') {
-          dispatch(createTopic({ data: schemaData, metalake, catalog, schema: 
schemaName, type: catalogType })).then(
+          dispatch(registerModel({ data: schemaData, metalake, catalog, 
schema: schemaName, type: catalogType })).then(
             res => {
               if (!res.payload?.err) {
                 handleClose()
               }
             }
           )
-        } else {
-          const reqData = { updates: genUpdates(cacheData, schemaData) }
-
-          if (reqData.updates.length !== 0) {
-            dispatch(
-              updateTopic({
-                metalake,
-                catalog,
-                type: catalogType,
-                schema: schemaName,
-                topic: cacheData.name,
-                data: reqData
-              })
-            ).then(res => {
-              if (!res.payload?.err) {
-                handleClose()
-              }
-            })
-          }
         }
       })
       .catch(err => {
@@ -273,7 +241,7 @@ const CreateTopicDialog = props => {
           </IconButton>
           <Box sx={{ mb: 8, textAlign: 'center' }}>
             <Typography variant='h5' sx={{ mb: 3 }}>
-              {type === 'create' ? 'Create' : 'Edit'} Topic
+              {type === 'create' ? 'Register' : 'Edit'} Model
             </Typography>
           </Box>
 
@@ -292,7 +260,7 @@ const CreateTopicDialog = props => {
                       placeholder=''
                       disabled={type === 'update'}
                       error={Boolean(errors.name)}
-                      data-refer='topic-name-field'
+                      data-refer='model-name-field'
                     />
                   )}
                 />
@@ -315,14 +283,14 @@ const CreateTopicDialog = props => {
                       onChange={onChange}
                       placeholder=''
                       error={Boolean(errors.comment)}
-                      data-refer='topic-comment-field'
+                      data-refer='model-comment-field'
                     />
                   )}
                 />
               </FormControl>
             </Grid>
 
-            <Grid item xs={12} data-refer='topic-props-layout'>
+            <Grid item xs={12} data-refer='model-props-layout'>
               <Typography sx={{ mb: 2 }} variant='body2'>
                 Properties
               </Typography>
@@ -334,7 +302,7 @@ const CreateTopicDialog = props => {
                         <Box>
                           <Box
                             sx={{ display: 'flex', alignItems: 'center', 
justifyContent: 'space-between' }}
-                            data-refer={`topic-props-${index}`}
+                            data-refer={`model-props-${index}`}
                           >
                             <Box>
                               <TextField
@@ -406,7 +374,7 @@ const CreateTopicDialog = props => {
                 onClick={addFields}
                 variant='outlined'
                 startIcon={<Icon icon='mdi:plus-circle-outline' />}
-                data-refer='add-topic-props'
+                data-refer='add-model-props'
               >
                 Add Property
               </Button>
@@ -420,8 +388,8 @@ const CreateTopicDialog = props => {
             pb: theme => [`${theme.spacing(5)} !important`, 
`${theme.spacing(12.5)} !important`]
           }}
         >
-          <Button variant='contained' sx={{ mr: 1 }} type='submit' 
data-refer='handle-submit-topic'>
-            {type === 'create' ? 'Create' : 'Update'}
+          <Button variant='contained' sx={{ mr: 1 }} type='submit' 
data-refer='handle-submit-model'>
+            {type === 'create' ? 'Register' : 'Update'}
           </Button>
           <Button variant='outlined' onClick={handleClose}>
             Cancel
@@ -432,4 +400,4 @@ const CreateTopicDialog = props => {
   )
 }
 
-export default CreateTopicDialog
+export default RegisterModelDialog
diff --git a/web/web/src/app/metalakes/metalake/rightContent/RightContent.js 
b/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
index 54b248297..1495ae3c5 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/RightContent.js
@@ -29,6 +29,8 @@ import CreateSchemaDialog from './CreateSchemaDialog'
 import CreateFilesetDialog from './CreateFilesetDialog'
 import CreateTopicDialog from './CreateTopicDialog'
 import CreateTableDialog from './CreateTableDialog'
+import RegisterModelDialog from './RegisterModelDialog'
+import LinkVersionDialog from './LinkVersionDialog'
 import TabsContent from './tabsContent/TabsContent'
 import { useSearchParams } from 'next/navigation'
 import { useAppSelector } from '@/lib/hooks/useStore'
@@ -39,12 +41,16 @@ const RightContent = () => {
   const [openFileset, setOpenFileset] = useState(false)
   const [openTopic, setOpenTopic] = useState(false)
   const [openTable, setOpenTable] = useState(false)
+  const [openModel, setOpenModel] = useState(false)
+  const [openVersion, setOpenVersion] = useState(false)
   const searchParams = useSearchParams()
   const [isShowBtn, setBtnVisible] = useState(true)
   const [isShowSchemaBtn, setSchemaBtnVisible] = useState(false)
   const [isShowFilesetBtn, setFilesetBtnVisible] = useState(false)
   const [isShowTopicBtn, setTopicBtnVisible] = useState(false)
   const [isShowTableBtn, setTableBtnVisible] = useState(false)
+  const [isShowModelBtn, setModelBtnVisible] = useState(false)
+  const [isShowVersionBtn, setVersionBtnVisible] = useState(false)
   const store = useAppSelector(state => state.metalakes)
 
   const handleCreateCatalog = () => {
@@ -67,6 +73,14 @@ const RightContent = () => {
     setOpenTable(true)
   }
 
+  const handleRegisterModel = () => {
+    setOpenModel(true)
+  }
+
+  const handleLinkVersion = () => {
+    setOpenVersion(true)
+  }
+
   useEffect(() => {
     const paramsSize = [...searchParams.keys()].length
     const isCatalogList = paramsSize == 1 && searchParams.get('metalake')
@@ -88,6 +102,23 @@ const RightContent = () => {
       searchParams.has('schema')
     setTopicBtnVisible(isTopicList)
 
+    const isModelList =
+      paramsSize == 4 &&
+      searchParams.has('metalake') &&
+      searchParams.has('catalog') &&
+      searchParams.get('type') === 'model' &&
+      searchParams.has('schema')
+    setModelBtnVisible(isModelList)
+
+    const isVersionList =
+      paramsSize == 5 &&
+      searchParams.has('metalake') &&
+      searchParams.has('catalog') &&
+      searchParams.get('type') === 'model' &&
+      searchParams.has('schema') &&
+      searchParams.has('model')
+    setVersionBtnVisible(isVersionList)
+
     if (store.catalogs.length) {
       const currentCatalog = store.catalogs.filter(ca => ca.name === 
searchParams.get('catalog'))[0]
 
@@ -199,6 +230,34 @@ const RightContent = () => {
             <CreateTableDialog open={openTable} setOpen={setOpenTable} />
           </Box>
         )}
+        {isShowModelBtn && (
+          <Box className={`twc-flex twc-items-center`}>
+            <Button
+              variant='contained'
+              startIcon={<Icon icon='mdi:plus-box' />}
+              onClick={handleRegisterModel}
+              sx={{ width: 200 }}
+              data-refer='register-model-btn'
+            >
+              Register Model
+            </Button>
+            <RegisterModelDialog open={openModel} setOpen={setOpenModel} />
+          </Box>
+        )}
+        {isShowVersionBtn && (
+          <Box className={`twc-flex twc-items-center`}>
+            <Button
+              variant='contained'
+              startIcon={<Icon icon='mdi:plus-box' />}
+              onClick={handleLinkVersion}
+              sx={{ width: 200 }}
+              data-refer='link-version-btn'
+            >
+              Link Version
+            </Button>
+            <LinkVersionDialog open={openVersion} setOpen={setOpenVersion} />
+          </Box>
+        )}
       </Box>
 
       <Box sx={{ height: 'calc(100% - 4.1rem)' }}>
diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js 
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js
index 9e45da054..55f2db690 100644
--- a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js
+++ b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/TabsContent.js
@@ -85,7 +85,10 @@ const TabsContent = () => {
   const paramsSize = [...searchParams.keys()].length
   const type = searchParams.get('type')
   const [tab, setTab] = useState('table')
-  const isNotNeedTableTab = type && ['fileset', 'messaging'].includes(type) && 
paramsSize === 5
+
+  const isNotNeedTableTab =
+    (type && ['fileset', 'messaging'].includes(type) && paramsSize === 5) ||
+    (paramsSize === 6 && searchParams.get('version'))
   const isShowTableProps = paramsSize === 5 && !['fileset', 
'messaging'].includes(type)
 
   const handleChangeTab = (event, newValue) => {
@@ -101,18 +104,29 @@ const TabsContent = () => {
       break
     case 4:
       switch (type) {
+        case 'relational':
+          tableTitle = 'Tables'
+          break
         case 'fileset':
           tableTitle = 'Filesets'
           break
         case 'messaging':
           tableTitle = 'Topics'
           break
-        default:
-          tableTitle = 'Tables'
+        case 'model':
+          tableTitle = 'Models'
+          break
       }
       break
     case 5:
-      tableTitle = 'Columns'
+      switch (type) {
+        case 'relational':
+          tableTitle = 'Columns'
+          break
+        case 'model':
+          tableTitle = 'Versions'
+          break
+      }
       break
     default:
       break
diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
 
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
index 6e21eabdb..580c49717 100644
--- 
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
+++ 
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/detailsView/DetailsView.js
@@ -104,6 +104,25 @@ const DetailsView = () => {
             </Grid>
           </>
         ) : null}
+
+        {activatedItem?.uri && (
+          <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
+            <Typography variant='body2' sx={{ mb: 2 }}>
+              URI
+            </Typography>
+            {renderFieldText({ value: activatedItem?.uri })}
+          </Grid>
+        )}
+
+        {activatedItem?.aliases && (
+          <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
+            <Typography variant='body2' sx={{ mb: 2 }}>
+              Aliases
+            </Typography>
+            {renderFieldText({ value: activatedItem?.aliases?.join(', ') })}
+          </Grid>
+        )}
+
         <Grid item xs={12} sx={{ mb: [0, 5] }}>
           <Typography variant='body2' sx={{ mb: 2 }}>
             Comment
diff --git 
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js
 
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js
index a2a73c1ec..12716677e 100644
--- 
a/web/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js
+++ 
b/web/web/src/app/metalakes/metalake/rightContent/tabsContent/tableView/TableView.js
@@ -52,6 +52,8 @@ import {
   deleteTopic,
   deleteSchema,
   deleteTable,
+  deleteModel,
+  deleteVersion,
   setCatalogInUse
 } from '@/lib/store/metalakes'
 
@@ -62,6 +64,7 @@ import { useSearchParams } from 'next/navigation'
 import { getFilesetDetailsApi } from '@/lib/api/filesets'
 import { getTopicDetailsApi } from '@/lib/api/topics'
 import { getTableDetailsApi } from '@/lib/api/tables'
+import { getModelDetailsApi, getVersionDetailsApi } from '@/lib/api/models'
 
 const fonts = Inconsolata({ subsets: ['latin'] })
 
@@ -90,6 +93,7 @@ const TableView = () => {
   const catalog = searchParams.get('catalog') || ''
   const type = searchParams.get('type') || ''
   const schema = searchParams.get('schema') || ''
+  const model = searchParams.get('model') || ''
 
   const isCatalogList = paramsSize == 1 && searchParams.has('metalake')
 
@@ -119,6 +123,7 @@ const TableView = () => {
   const [dialogData, setDialogData] = useState({})
   const [dialogType, setDialogType] = useState('create')
   const [isHideEdit, setIsHideEdit] = useState(true)
+  const [isHideDrop, setIsHideDrop] = useState(true)
 
   useEffect(() => {
     if (store.catalogs.length) {
@@ -127,9 +132,10 @@ const TableView = () => {
       const isHideAction =
         (['lakehouse-hudi', 'kafka'].includes(currentCatalog?.provider) && 
paramsSize == 3) ||
         (currentCatalog?.provider === 'lakehouse-hudi' && paramsSize == 4)
-      setIsHideEdit(isHideAction)
+      setIsHideEdit(isHideAction || type === 'model')
+      setIsHideDrop(isHideAction)
     }
-  }, [store.catalogs, store.catalogs.length, paramsSize, catalog])
+  }, [store.catalogs, store.catalogs.length, paramsSize, catalog, type])
 
   const handleClickUrl = path => {
     if (!path) {
@@ -249,7 +255,7 @@ const TableView = () => {
       disableColumnMenu: true,
       type: 'string',
       field: 'name',
-      headerName: 'Name',
+      headerName: model ? 'Version' : 'Name',
       renderCell: ({ row }) => {
         const { name, path } = row
 
@@ -325,7 +331,7 @@ const TableView = () => {
             </IconButton>
           )}
 
-          {!isHideEdit && (
+          {!isHideDrop && (
             <IconButton
               title='Delete'
               size='small'
@@ -535,6 +541,26 @@ const TableView = () => {
         setOpenDrawer(true)
         break
       }
+      case 'model': {
+        const [err, res] = await to(getModelDetailsApi({ metalake, catalog, 
schema, model: row.name }))
+        if (err || !res) {
+          throw new Error(err)
+        }
+
+        setDrawerData(res.model)
+        setOpenDrawer(true)
+        break
+      }
+      case 'version': {
+        const [err, res] = await to(getVersionDetailsApi({ metalake, catalog, 
schema, model, version: row.name }))
+        if (err || !res) {
+          throw new Error(err)
+        }
+
+        setDrawerData(res.modelVersion)
+        setOpenDrawer(true)
+        break
+      }
       default:
         return
     }
@@ -642,6 +668,12 @@ const TableView = () => {
         case 'table':
           dispatch(deleteTable({ metalake, catalog, type, schema, table: 
confirmCacheData.name }))
           break
+        case 'model':
+          dispatch(deleteModel({ metalake, catalog, type, schema, model: 
confirmCacheData.name }))
+          break
+        case 'version':
+          dispatch(deleteVersion({ metalake, catalog, type, schema, model, 
version: confirmCacheData.name }))
+          break
         default:
           break
       }
@@ -676,7 +708,18 @@ const TableView = () => {
         searchParams.has('metalake') &&
         searchParams.has('catalog') &&
         searchParams.get('type') === 'relational' &&
-        searchParams.has('schema'))
+        searchParams.has('schema')) ||
+      (paramsSize == 4 &&
+        searchParams.has('metalake') &&
+        searchParams.has('catalog') &&
+        searchParams.get('type') === 'model' &&
+        searchParams.has('schema')) ||
+      (paramsSize == 5 &&
+        searchParams.has('metalake') &&
+        searchParams.has('catalog') &&
+        searchParams.get('type') === 'model' &&
+        searchParams.has('schema') &&
+        searchParams.has('model'))
     ) {
       return actionsColumns
     } else if (paramsSize == 5 && searchParams.has('table')) {
diff --git a/web/web/src/components/DetailsDrawer.js 
b/web/web/src/components/DetailsDrawer.js
index a3cc707fa..740c7ae15 100644
--- a/web/web/src/components/DetailsDrawer.js
+++ b/web/web/src/components/DetailsDrawer.js
@@ -121,10 +121,28 @@ const DetailsDrawer = props => {
             }}
             data-refer='details-title'
           >
-            {drawerData.name}
+            {drawerData.name || drawerData.version}
           </Typography>
         </Grid>
 
+        {drawerData.uri && (
+          <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
+            <Typography variant='body2' sx={{ mb: 2 }}>
+              Type
+            </Typography>
+            {renderFieldText({ value: drawerData.uri })}
+          </Grid>
+        )}
+
+        {drawerData.aliases && (
+          <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
+            <Typography variant='body2' sx={{ mb: 2 }}>
+              Aliases
+            </Typography>
+            {renderFieldText({ value: drawerData.aliases.join(', ') })}
+          </Grid>
+        )}
+
         {drawerData.type && (
           <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
             <Typography variant='body2' sx={{ mb: 2 }}>
@@ -134,7 +152,7 @@ const DetailsDrawer = props => {
           </Grid>
         )}
 
-        {drawerData.provider && (
+        {drawerData.provider && drawerData?.type !== 'model' && (
           <Grid item xs={12} md={6} sx={{ mb: [0, 5] }}>
             <Typography variant='body2' sx={{ mb: 2 }}>
               Provider
diff --git a/web/web/src/lib/api/models/index.js 
b/web/web/src/lib/api/models/index.js
new file mode 100644
index 000000000..fa968326d
--- /dev/null
+++ b/web/web/src/lib/api/models/index.js
@@ -0,0 +1,100 @@
+/*
+ * 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 { defHttp } from '@/lib/utils/axios'
+
+const Apis = {
+  GET: ({ metalake, catalog, schema }) =>
+    
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+      catalog
+    )}/schemas/${encodeURIComponent(schema)}/models`,
+  GET_DETAIL: ({ metalake, catalog, schema, model }) =>
+    
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+      catalog
+    
)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}`,
+  REGISTER: ({ metalake, catalog, schema }) =>
+    
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}/models`,
+  UPDATE: ({ metalake, catalog, schema, model }) =>
+    
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}`,
+  DELETE: ({ metalake, catalog, schema, model }) =>
+    
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(catalog)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}`,
+  GET_VERSIONS: ({ metalake, catalog, schema, model }) =>
+    
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+      catalog
+    
)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}/versions`,
+  GET_VERSION_DETAIL: ({ metalake, catalog, schema, model, version }) =>
+    
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+      catalog
+    
)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}/versions/${version}`,
+  LINK_VERSION: ({ metalake, catalog, schema, model }) =>
+    
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+      catalog
+    
)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}`,
+  DELETE_VERSION: ({ metalake, catalog, schema, model, version }) => {
+    return 
`/api/metalakes/${encodeURIComponent(metalake)}/catalogs/${encodeURIComponent(
+      catalog
+    
)}/schemas/${encodeURIComponent(schema)}/models/${encodeURIComponent(model)}/versions/${version}`
+  }
+}
+
+export const getModelsApi = params => {
+  return defHttp.get({
+    url: `${Apis.GET(params)}`
+  })
+}
+
+export const getModelDetailsApi = ({ metalake, catalog, schema, model }) => {
+  return defHttp.get({
+    url: `${Apis.GET_DETAIL({ metalake, catalog, schema, model })}`
+  })
+}
+
+export const registerModelApi = ({ metalake, catalog, schema, data }) => {
+  return defHttp.post({ url: `${Apis.REGISTER({ metalake, catalog, schema 
})}`, data })
+}
+
+export const updateModelApi = ({ metalake, catalog, schema, model, data }) => {
+  return defHttp.put({ url: `${Apis.UPDATE({ metalake, catalog, schema, model 
})}`, data })
+}
+
+export const deleteModelApi = ({ metalake, catalog, schema, model }) => {
+  return defHttp.delete({ url: `${Apis.DELETE({ metalake, catalog, schema, 
model })}` })
+}
+
+export const getModelVersionsApi = params => {
+  return defHttp.get({
+    url: `${Apis.GET_VERSIONS(params)}`
+  })
+}
+
+export const linkVersionApi = ({ metalake, catalog, schema, model, data }) => {
+  return defHttp.post({ url: `${Apis.LINK_VERSION({ metalake, catalog, schema, 
model })}`, data })
+}
+
+export const getVersionDetailsApi = ({ metalake, catalog, schema, model, 
version }) => {
+  return defHttp.get({
+    url: `${Apis.GET_VERSION_DETAIL({ metalake, catalog, schema, model, 
version })}`
+  })
+}
+
+export const deleteVersionApi = ({ metalake, catalog, schema, model, version 
}) => {
+  return defHttp.delete({
+    url: `${Apis.DELETE_VERSION({ metalake, catalog, schema, model, version 
})}`
+  })
+}
diff --git a/web/web/src/lib/store/metalakes/index.js 
b/web/web/src/lib/store/metalakes/index.js
index 3d4ad454d..2ff3322b8 100644
--- a/web/web/src/lib/store/metalakes/index.js
+++ b/web/web/src/lib/store/metalakes/index.js
@@ -55,6 +55,17 @@ import {
   deleteFilesetApi
 } from '@/lib/api/filesets'
 import { getTopicsApi, getTopicDetailsApi, createTopicApi, updateTopicApi, 
deleteTopicApi } from '@/lib/api/topics'
+import {
+  getModelsApi,
+  getModelDetailsApi,
+  registerModelApi,
+  updateModelApi,
+  deleteModelApi,
+  getModelVersionsApi,
+  getVersionDetailsApi,
+  linkVersionApi,
+  deleteVersionApi
+} from '@/lib/api/models'
 
 export const fetchMetalakes = createAsyncThunk('appMetalakes/fetchMetalakes', 
async (params, { getState }) => {
   const [err, res] = await to(getMetalakesApi())
@@ -116,7 +127,7 @@ export const setIntoTreeNodeWithFetch = createAsyncThunk(
     }
 
     const pathArr = extractPlaceholder(key)
-    const [metalake, catalog, type, schema] = pathArr
+    const [metalake, catalog, type, schema, entity] = pathArr
 
     if (pathArr.length === 1) {
       const [err, res] = await to(getCatalogsApi({ metalake }))
@@ -250,6 +261,27 @@ export const setIntoTreeNodeWithFetch = createAsyncThunk(
           isLeaf: true
         }
       })
+    } else if (pathArr.length === 4 && type === 'model') {
+      const [err, res] = await to(getModelsApi({ metalake, catalog, schema }))
+
+      if (err || !res) {
+        throw new Error(err)
+      }
+
+      const { identifiers = [] } = res
+
+      result.data = identifiers.map(modelItem => {
+        return {
+          ...modelItem,
+          node: 'model',
+          id: 
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${modelItem.name}}}`,
+          key: 
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${modelItem.name}}}`,
+          path: `?${new URLSearchParams({ metalake, catalog, type, schema, 
model: modelItem.name }).toString()}`,
+          name: modelItem.name,
+          title: modelItem.name,
+          isLeaf: true
+        }
+      })
     }
 
     return result
@@ -602,7 +634,7 @@ export const updateSchema = createAsyncThunk(
     }
     dispatch(fetchSchemas({ metalake, catalog, type, init: true }))
 
-    return res.catalog
+    return res.schema
   }
 )
 
@@ -842,7 +874,7 @@ export const updateTable = createAsyncThunk(
     }
     dispatch(fetchTables({ metalake, catalog, type, schema, init: true }))
 
-    return res.catalog
+    return res.table
   }
 )
 
@@ -993,7 +1025,7 @@ export const updateFileset = createAsyncThunk(
     }
     dispatch(fetchFilesets({ metalake, catalog, type, schema, init: true }))
 
-    return res.catalog
+    return res.fileset
   }
 )
 
@@ -1144,7 +1176,7 @@ export const updateTopic = createAsyncThunk(
     }
     dispatch(fetchTopics({ metalake, catalog, type, schema, init: true }))
 
-    return res.catalog
+    return res.topic
   }
 )
 
@@ -1165,6 +1197,248 @@ export const deleteTopic = createAsyncThunk(
   }
 )
 
+export const fetchModels = createAsyncThunk(
+  'appMetalakes/fetchModels',
+  async ({ init, page, metalake, catalog, schema }, { getState, dispatch }) => 
{
+    if (init) {
+      dispatch(setTableLoading(true))
+    }
+
+    const [err, res] = await to(getModelsApi({ metalake, catalog, schema }))
+    dispatch(setTableLoading(false))
+
+    if (init && (err || !res)) {
+      dispatch(resetTableData())
+      throw new Error(err)
+    }
+
+    const { identifiers = [] } = res
+
+    const models = identifiers.map(model => {
+      return {
+        ...model,
+        node: 'model',
+        id: 
`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}{{${model.name}}}`,
+        key: 
`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}{{${model.name}}}`,
+        path: `?${new URLSearchParams({
+          metalake,
+          catalog,
+          type: 'model',
+          schema,
+          model: model.name
+        }).toString()}`,
+        name: model.name,
+        title: model.name,
+        isLeaf: true
+      }
+    })
+
+    if (init && 
getState().metalakes.loadedNodes.includes(`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}`))
 {
+      dispatch(
+        setIntoTreeNodes({
+          key: `{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}`,
+          data: models,
+          tree: getState().metalakes.metalakeTree
+        })
+      )
+    }
+
+    dispatch(
+      setExpandedNodes([
+        `{{${metalake}}}`,
+        `{{${metalake}}}{{${catalog}}}{{${'model'}}}`,
+        `{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}`
+      ])
+    )
+
+    return { models, page, init }
+  }
+)
+
+export const getModelDetails = createAsyncThunk(
+  'appMetalakes/getModelDetails',
+  async ({ init, metalake, catalog, schema, model }, { getState, dispatch }) 
=> {
+    const [err, res] = await to(getModelDetailsApi({ metalake, catalog, 
schema, model }))
+
+    if (err || !res) {
+      throw new Error(err)
+    }
+
+    const { model: resModel } = res
+
+    return resModel
+  }
+)
+
+export const registerModel = createAsyncThunk(
+  'appMetalakes/registerModel',
+  async ({ data, metalake, catalog, type, schema }, { dispatch }) => {
+    dispatch(setTableLoading(true))
+    const [err, res] = await to(registerModelApi({ data, metalake, catalog, 
schema }))
+    dispatch(setTableLoading(false))
+
+    if (err || !res) {
+      return { err: true }
+    }
+
+    const { model: modelItem } = res
+
+    const modelData = {
+      ...modelItem,
+      node: 'model',
+      id: 
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${modelItem.name}}}`,
+      key: 
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${modelItem.name}}}`,
+      path: `?${new URLSearchParams({ metalake, catalog, type, schema, model: 
modelItem.name }).toString()}`,
+      name: modelItem.name,
+      title: modelItem.name,
+      tables: [],
+      children: []
+    }
+
+    dispatch(fetchModels({ metalake, catalog, schema, type, init: true }))
+
+    return modelData
+  }
+)
+
+export const updateModel = createAsyncThunk(
+  'appMetalakes/updateModel',
+  async ({ metalake, catalog, type, schema, model, data }, { dispatch }) => {
+    const [err, res] = await to(updateTopicApi({ metalake, catalog, schema, 
model, data }))
+    if (err || !res) {
+      return { err: true }
+    }
+    dispatch(fetchModels({ metalake, catalog, type, schema, init: true }))
+
+    return res.model
+  }
+)
+
+export const deleteModel = createAsyncThunk(
+  'appMetalakes/deleteModel',
+  async ({ metalake, catalog, type, schema, model }, { dispatch }) => {
+    dispatch(setTableLoading(true))
+    const [err, res] = await to(deleteModleApi({ metalake, catalog, schema, 
model }))
+    dispatch(setTableLoading(false))
+
+    if (err || !res) {
+      throw new Error(err)
+    }
+
+    dispatch(fetchModels({ metalake, catalog, type, schema, page: 'models', 
init: true }))
+
+    return res
+  }
+)
+
+export const fetchModelVersions = createAsyncThunk(
+  'appMetalakes/fetchModelVersions',
+  async ({ init, page, metalake, catalog, schema, model }, { getState, 
dispatch }) => {
+    if (init) {
+      dispatch(setTableLoading(true))
+    }
+
+    const [err, res] = await to(getModelVersionsApi({ metalake, catalog, 
schema, model }))
+    dispatch(setTableLoading(false))
+
+    if (init && (err || !res)) {
+      dispatch(resetTableData())
+      throw new Error(err)
+    }
+
+    const { versions = [] } = res
+
+    const versionsData = versions.map(version => {
+      return {
+        node: 'version',
+        id: 
`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}{{${model}}}{{${version}}}`,
+        key: 
`{{${metalake}}}{{${catalog}}}{{${'model'}}}{{${schema}}}{{${model}}}{{${version}}}`,
+        path: `?${new URLSearchParams({
+          metalake,
+          catalog,
+          type: 'model',
+          schema,
+          model,
+          version
+        }).toString()}`,
+        name: version,
+        title: version,
+        isLeaf: true
+      }
+    })
+
+    return { versions: versionsData, page, init }
+  }
+)
+
+export const getVersionDetails = createAsyncThunk(
+  'appMetalakes/getVersionDetails',
+  async ({ init, metalake, catalog, schema, model, version }, { getState, 
dispatch }) => {
+    dispatch(resetTableData())
+    if (init) {
+      dispatch(setTableLoading(true))
+    }
+    const [err, res] = await to(getVersionDetailsApi({ metalake, catalog, 
schema, model, version }))
+    dispatch(setTableLoading(false))
+
+    if (err || !res) {
+      dispatch(resetTableData())
+      throw new Error(err)
+    }
+
+    const { modelVersion } = res
+
+    return modelVersion
+  }
+)
+
+export const linkVersion = createAsyncThunk(
+  'appMetalakes/linkVersion',
+  async ({ data, metalake, catalog, type, schema, model }, { dispatch }) => {
+    dispatch(setTableLoading(true))
+    const [err, res] = await to(linkVersionApi({ data, metalake, catalog, 
schema, model }))
+    dispatch(setTableLoading(false))
+
+    if (err || !res) {
+      return { err: true }
+    }
+
+    const { version: versionItem } = res
+
+    const versionData = {
+      node: 'version',
+      id: 
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${model}}}{{${versionItem}}}`,
+      key: 
`{{${metalake}}}{{${catalog}}}{{${type}}}{{${schema}}}{{${model}}}{{${versionItem}}}`,
+      path: `?${new URLSearchParams({ metalake, catalog, type, schema, 
version: versionItem }).toString()}`,
+      name: versionItem,
+      title: versionItem,
+      tables: [],
+      children: []
+    }
+
+    dispatch(fetchModelVersions({ metalake, catalog, schema, type, model, 
init: true }))
+
+    return versionData
+  }
+)
+
+export const deleteVersion = createAsyncThunk(
+  'appMetalakes/deleteVersion',
+  async ({ metalake, catalog, type, schema, model, version }, { dispatch }) => 
{
+    dispatch(setTableLoading(true))
+    const [err, res] = await to(deleteVersionApi({ metalake, catalog, schema, 
model, version }))
+    dispatch(setTableLoading(false))
+
+    if (err || !res) {
+      throw new Error(err)
+    }
+
+    dispatch(fetchModelVersions({ metalake, catalog, type, schema, model, 
page: 'versions', init: true }))
+
+    return res
+  }
+)
+
 export const appMetalakesSlice = createSlice({
   name: 'appMetalakes',
   initialState: {
@@ -1178,6 +1452,8 @@ export const appMetalakesSlice = createSlice({
     columns: [],
     filesets: [],
     topics: [],
+    models: [],
+    versions: [],
     metalakeTree: [],
     loadedNodes: [],
     selectedNodes: [],
@@ -1232,6 +1508,8 @@ export const appMetalakesSlice = createSlice({
       state.columns = []
       state.filesets = []
       state.topics = []
+      state.models = []
+      state.versions = []
     },
     setTableLoading(state, action) {
       state.tableLoading = action.payload
@@ -1492,6 +1770,64 @@ export const appMetalakesSlice = createSlice({
         toast.error(action.error.message)
       }
     })
+    builder.addCase(fetchModels.fulfilled, (state, action) => {
+      state.models = action.payload.models
+      if (action.payload.init) {
+        state.tableData = action.payload.models
+      }
+    })
+    builder.addCase(fetchModels.rejected, (state, action) => {
+      if (!action.error.message.includes('CanceledError')) {
+        toast.error(action.error.message)
+      }
+    })
+    builder.addCase(getModelDetails.fulfilled, (state, action) => {
+      state.activatedDetails = action.payload
+    })
+    builder.addCase(getModelDetails.rejected, (state, action) => {
+      if (!action.error.message.includes('CanceledError')) {
+        toast.error(action.error.message)
+      }
+    })
+    builder.addCase(registerModel.rejected, (state, action) => {
+      if (!action.error.message.includes('CanceledError')) {
+        toast.error(action.error.message)
+      }
+    })
+    builder.addCase(updateModel.rejected, (state, action) => {
+      if (!action.error.message.includes('CanceledError')) {
+        toast.error(action.error.message)
+      }
+    })
+    builder.addCase(deleteModel.rejected, (state, action) => {
+      if (!action.error.message.includes('CanceledError')) {
+        toast.error(action.error.message)
+      }
+    })
+    builder.addCase(fetchModelVersions.fulfilled, (state, action) => {
+      state.versions = action.payload.versions
+      if (action.payload.init) {
+        state.tableData = action.payload.versions
+      }
+    })
+    builder.addCase(fetchModelVersions.rejected, (state, action) => {
+      if (!action.error.message.includes('CanceledError')) {
+        toast.error(action.error.message)
+      }
+    })
+    builder.addCase(getVersionDetails.fulfilled, (state, action) => {
+      state.activatedDetails = action.payload
+    })
+    builder.addCase(getVersionDetails.rejected, (state, action) => {
+      if (!action.error.message.includes('CanceledError')) {
+        toast.error(action.error.message)
+      }
+    })
+    builder.addCase(linkVersion.rejected, (state, action) => {
+      if (!action.error.message.includes('CanceledError')) {
+        toast.error(action.error.message)
+      }
+    })
   }
 })
 


Reply via email to