You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by yi...@apache.org on 2025/08/28 05:33:35 UTC

(texera) 01/05: feat: add model

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

yiconghuang pushed a commit to branch feat/add-model
in repository https://gitbox.apache.org/repos/asf/texera.git

commit f3df59c59c8a512765a398aa49936514c0c788ee
Author: Yicong Huang <17...@users.noreply.github.com>
AuthorDate: Sun Aug 24 02:46:15 2025 -0700

    feat: add model
---
 .../web/resource/dashboard/DashboardResource.scala |  19 +-
 .../dashboard/ModelSearchQueryBuilder.scala        | 144 ++++
 .../resource/dashboard/SearchQueryBuilder.scala    |   1 +
 .../resource/dashboard/UnifiedResourceSchema.scala |  10 +-
 .../web/resource/dashboard/hub/EntityTables.scala  |  17 +
 .../web/resource/dashboard/hub/EntityType.scala    |   4 +-
 .../web/resource/dashboard/hub/HubResource.scala   |  24 +-
 .../dashboard/user/model/ModelResource.scala}      |  23 +-
 core/config/src/main/resources/default.conf        |   1 +
 core/config/src/main/resources/storage.conf        |   2 +-
 core/config/src/main/resources/user-system.conf    |   4 +-
 .../scala/edu/uci/ics/texera/dao/SqlServer.scala   |   2 +
 .../uci/ics/texera/dao/jooq/generated/Keys.java    |  22 +
 .../uci/ics/texera/dao/jooq/generated/Tables.java  |  30 +
 .../ics/texera/dao/jooq/generated/TexeraDb.java    |  35 +
 .../texera/dao/jooq/generated/tables/Dataset.java  |  12 +-
 .../dao/jooq/generated/tables/daos/DatasetDao.java |  30 +-
 .../jooq/generated/tables/interfaces/IDataset.java |  20 +-
 .../dao/jooq/generated/tables/pojos/Dataset.java   |  46 +-
 .../generated/tables/records/DatasetRecord.java    |  94 +--
 .../edu/uci/ics/texera/service/FileService.scala   |   8 +-
 .../texera/service/resource/DatasetResource.scala  |   8 +-
 .../service/resource/ModelAccessResource.scala     | 221 ++++++
 .../{DatasetResource.scala => ModelResource.scala} | 759 ++++++++++-----------
 core/gui/proxy.config.json                         |   5 +
 core/gui/src/app/app-routing.constant.ts           |   1 +
 core/gui/src/app/app-routing.module.ts             |   5 +
 core/gui/src/app/app.module.ts                     |  10 +
 core/gui/src/app/common/type/dataset.ts            |  12 +
 core/gui/src/app/common/type/gui-config.ts         |   1 +
 .../admin/settings/admin-settings.component.ts     |   1 +
 .../dashboard/component/dashboard.component.html   |  12 +
 .../app/dashboard/component/dashboard.component.ts |   4 +-
 .../model-detail.component.html                    | 336 +++++++++
 .../model-detail.component.scss                    | 247 +++++++
 .../model-detail.component.ts                      | 567 +++++++++++++++
 .../user-dataset-file-renderer.component.html      | 104 +++
 .../user-dataset-file-renderer.component.scss}     |  30 +-
 .../user-dataset-file-renderer.component.spec.ts   |  57 ++
 .../user-dataset-file-renderer.component.ts        | 347 ++++++++++
 ...user-dataset-staged-objects-list.component.html |  58 ++
 ...ser-dataset-staged-objects-list.component.scss} |  38 +-
 .../user-dataset-staged-objects-list.component.ts  |  94 +++
 .../user-model-version-creator.component.html      |  72 ++
 .../user-model-version-creator.component.scss      |  86 +++
 .../user-model-version-creator.component.ts        | 220 ++++++
 .../user-dataset-version-filetree.component.html   |  54 ++
 .../user-dataset-version-filetree.component.scss}  |  42 +-
 .../user-dataset-version-filetree.component.ts     |  77 +++
 .../user-dataset-list-item.component.html          | 154 +++++
 .../user-dataset-list-item.component.scss          |  87 +++
 .../user-dataset-list-item.component.ts            | 133 ++++
 .../user/user-model/user-model.component.html      |  61 ++
 .../user/user-model/user-model.component.scss}     |  14 +-
 .../user/user-model/user-model.component.ts        | 207 ++++++
 .../service/user/download/download.service.spec.ts |   2 +-
 .../dashboard/service/user/model/model.service.ts  | 524 ++++++++++++++
 .../app/dashboard/service/user/search.service.ts   |  25 +-
 .../dashboard/type/dashboard-dataset.interface.ts  |  11 +-
 core/gui/src/app/dashboard/type/dashboard-entry.ts |  37 +-
 .../app/dashboard/type/search-filter-parameters.ts |   2 +-
 core/gui/src/app/dashboard/type/search-result.ts   |   5 +-
 core/gui/src/app/dashboard/type/type-predicates.ts |   6 +-
 core/gui/src/app/hub/service/hub.service.ts        |   1 +
 core/scripts/sql/texera_ddl.sql                    |  61 ++
 65 files changed, 4734 insertions(+), 612 deletions(-)

diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/DashboardResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/DashboardResource.scala
index 92341df0af..5a0d06ebad 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/DashboardResource.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/DashboardResource.scala
@@ -25,9 +25,9 @@ import edu.uci.ics.texera.dao.jooq.generated.tables.pojos._
 import edu.uci.ics.texera.web.resource.dashboard.DashboardResource._
 import edu.uci.ics.texera.web.resource.dashboard.SearchQueryBuilder.{ALL_RESOURCE_TYPE, context}
 import edu.uci.ics.texera.web.resource.dashboard.user.dataset.DatasetResource.DashboardDataset
+import edu.uci.ics.texera.web.resource.dashboard.user.model.ModelResource.DashboardModel
 import edu.uci.ics.texera.web.resource.dashboard.user.workflow.WorkflowResource.DashboardWorkflow
 import io.dropwizard.auth.Auth
-
 import org.jooq.{Field, OrderField}
 
 import java.util
@@ -40,7 +40,8 @@ object DashboardResource {
       resourceType: String,
       workflow: Option[DashboardWorkflow] = None,
       project: Option[Project] = None,
-      dataset: Option[DashboardDataset] = None
+      dataset: Option[DashboardDataset] = None,
+      model: Option[DashboardModel] =   None
   )
 
   case class UserInfo(userId: Integer, userName: String, googleAvatar: Option[String])
@@ -80,6 +81,7 @@ object DashboardResource {
       @QueryParam("operator") operators: java.util.List[String] = new util.ArrayList(),
       @QueryParam("projectId") projectIds: java.util.List[Integer] = new util.ArrayList(),
       @QueryParam("datasetId") datasetIds: java.util.List[Integer] = new util.ArrayList(),
+      @QueryParam("modelId") modelIds: java.util.List[Integer] = new util.ArrayList(),
       @QueryParam("start") @DefaultValue("0") offset: Int = 0,
       @QueryParam("count") @DefaultValue("20") count: Int = 20,
       @QueryParam("orderBy") @DefaultValue("EditTimeDesc") orderBy: String = "EditTimeDesc"
@@ -100,17 +102,22 @@ object DashboardResource {
         ProjectSearchQueryBuilder.constructQuery(uid, params, includePublic)
       case SearchQueryBuilder.DATASET_RESOURCE_TYPE =>
         DatasetSearchQueryBuilder.constructQuery(uid, params, includePublic)
+      case SearchQueryBuilder.MODEL_RESOURCE_TYPE =>
+        ModelSearchQueryBuilder.constructQuery(uid, params, includePublic)
       case SearchQueryBuilder.ALL_RESOURCE_TYPE =>
         val q1 = WorkflowSearchQueryBuilder.constructQuery(uid, params, includePublic)
         val q3 = ProjectSearchQueryBuilder.constructQuery(uid, params, includePublic)
         val q4 = DatasetSearchQueryBuilder.constructQuery(uid, params, includePublic)
-        q1.unionAll(q3).unionAll(q4)
+        val q5 = ModelSearchQueryBuilder.constructQuery(uid, params, includePublic)
+        q1.unionAll(q3).unionAll(q4).unionAll(q5)
       case _ => throw new IllegalArgumentException(s"Unknown resource type: ${params.resourceType}")
     }
 
     val finalQuery =
       query.orderBy(getOrderFields(params): _*).offset(params.offset).limit(params.count + 1)
+    println("SQL:" + finalQuery.getSQL)
     val queryResult = finalQuery.fetch()
+    println("Result size:" + queryResult.size())
 
     val allEntries = queryResult.asScala.toList
       .take(params.count)
@@ -121,6 +128,8 @@ object DashboardResource {
             WorkflowSearchQueryBuilder.toEntry(uid, record)
           case SearchQueryBuilder.PROJECT_RESOURCE_TYPE =>
             ProjectSearchQueryBuilder.toEntry(uid, record)
+          case SearchQueryBuilder.MODEL_RESOURCE_TYPE =>
+            ModelSearchQueryBuilder.toEntry(uid, record)
           case SearchQueryBuilder.DATASET_RESOURCE_TYPE =>
             DatasetSearchQueryBuilder.toEntry(uid, record)
         }
@@ -129,8 +138,8 @@ object DashboardResource {
     val entries = allEntries.filter(_ != null)
     val hasMismatch =
       params.resourceType match {
-        case SearchQueryBuilder.DATASET_RESOURCE_TYPE | SearchQueryBuilder.ALL_RESOURCE_TYPE =>
-          allEntries.exists(_ == null)
+        case SearchQueryBuilder.DATASET_RESOURCE_TYPE | SearchQueryBuilder.MODEL_RESOURCE_TYPE | SearchQueryBuilder.ALL_RESOURCE_TYPE =>
+          allEntries.contains(null)
         case _ =>
           false
       }
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/ModelSearchQueryBuilder.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/ModelSearchQueryBuilder.scala
new file mode 100644
index 0000000000..a3baf3ddda
--- /dev/null
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/ModelSearchQueryBuilder.scala
@@ -0,0 +1,144 @@
+/*
+ * 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.
+ */
+
+package edu.uci.ics.texera.web.resource.dashboard
+
+import com.typesafe.scalalogging.LazyLogging
+import edu.uci.ics.amber.core.storage.util.LakeFSStorageClient
+import edu.uci.ics.texera.dao.jooq.generated.Tables.{ MODEL, MODEL_USER_ACCESS}
+import edu.uci.ics.texera.dao.jooq.generated.enums.PrivilegeEnum
+import edu.uci.ics.texera.dao.jooq.generated.tables.User.USER
+import edu.uci.ics.texera.dao.jooq.generated.tables.pojos.{ Model, User}
+import edu.uci.ics.texera.web.resource.dashboard.DashboardResource.DashboardClickableFileEntry
+import edu.uci.ics.texera.web.resource.dashboard.FulltextSearchQueryUtils.{getContainsFilter, getDateFilter, getFullTextSearchFilter}
+import edu.uci.ics.texera.web.resource.dashboard.user.model.ModelResource.DashboardModel
+import org.jooq.impl.DSL
+import org.jooq.{Condition, GroupField, Record, TableLike}
+
+import scala.jdk.CollectionConverters.CollectionHasAsScala
+
+object ModelSearchQueryBuilder extends SearchQueryBuilder with LazyLogging {
+  override protected val mappedResourceSchema: UnifiedResourceSchema = UnifiedResourceSchema(
+    resourceType = DSL.inline(SearchQueryBuilder.MODEL_RESOURCE_TYPE),
+    name = MODEL.NAME,
+    description = MODEL.DESCRIPTION,
+    creationTime = MODEL.CREATION_TIME,
+    mid = MODEL.MID,
+    ownerId = MODEL.OWNER_UID,
+    isModelPublic= MODEL.IS_PUBLIC,
+    modelUserAccess = MODEL_USER_ACCESS.PRIVILEGE
+  )
+
+  /*
+   * constructs the FROM clause for querying models with specific access controls.
+   *
+   * Parameter:
+   * - uid: Integer - Represents the unique identifier of the current user.
+   *  - uid is 'null' if the user is not logged in or performing a public search.
+   *  - Otherwise, `uid` holds the identifier for the logged-in user.
+   * - includePublic - Boolean - Specifies whether to include public models in the result.
+   */
+  override protected def constructFromClause(
+      uid: Integer,
+      params: DashboardResource.SearchQueryParams,
+      includePublic: Boolean = false
+  ): TableLike[_] = {
+    val baseJoin = MODEL
+      .leftJoin(MODEL_USER_ACCESS)
+      .on(MODEL_USER_ACCESS.MID.eq(MODEL.MID))
+      .leftJoin(USER)
+      .on(USER.UID.eq(MODEL.OWNER_UID))
+
+    // Default condition starts as true, ensuring all models are selected initially.
+    var condition: Condition = DSL.trueCondition()
+
+    if (uid == null) {
+      // If `uid` is null, the user is not logged in or performing a public search
+      // We only select models marked as public
+      condition = MODEL.IS_PUBLIC.eq(true)
+    } else {
+      // When `uid` is present, we add a condition to only include models with direct user access.
+      val userAccessCondition = MODEL_USER_ACCESS.UID.eq(uid)
+
+      if (includePublic) {
+        // If `includePublic` is true, we extend visibility to public models as well.
+        condition = userAccessCondition.or(MODEL.IS_PUBLIC.eq(true))
+      } else {
+        condition = userAccessCondition
+      }
+    }
+    baseJoin.where(condition)
+  }
+
+  override protected def constructWhereClause(
+      uid: Integer,
+      params: DashboardResource.SearchQueryParams
+  ): Condition = {
+    val splitKeywords = params.keywords.asScala
+      .flatMap(_.split("[+\\-()<>~*@\"]"))
+      .filter(_.nonEmpty)
+      .toSeq
+
+    getDateFilter(
+      params.creationStartDate,
+      params.creationEndDate,
+      MODEL.CREATION_TIME
+    )
+      .and(getContainsFilter(params.modelIds, MODEL.MID))
+      .and(
+        getFullTextSearchFilter(splitKeywords, List(MODEL.NAME, MODEL.DESCRIPTION))
+      )
+  }
+
+  override protected def getGroupByFields: Seq[GroupField] = {
+    Seq.empty
+  }
+
+  override protected def toEntryImpl(
+      uid: Integer,
+      record: Record
+  ): DashboardResource.DashboardClickableFileEntry = {
+    val model = record.into(MODEL).into(classOf[Model])
+    val owner = record.into(USER).into(classOf[User])
+    var size = 0L
+
+    try {
+      size = LakeFSStorageClient.retrieveRepositorySize(model.getName)
+    } catch {
+      case e: io.lakefs.clients.sdk.ApiException =>
+        // Treat all LakeFS ApiException as mismatch (repository not found, being deleted, or any fatal error)
+        logger.error(s"LakeFS ApiException for model '${model.getName}': ${e.getMessage}", e)
+        return null
+    }
+
+    val dd = DashboardModel(
+      model,
+      owner.getEmail,
+      record.get(MODEL_USER_ACCESS.PRIVILEGE, classOf[PrivilegeEnum]),
+      model.getOwnerUid == uid,
+      size
+    )
+    DashboardClickableFileEntry(
+      resourceType = SearchQueryBuilder.MODEL_RESOURCE_TYPE,
+      model = Some(dd)
+    )
+  }
+}
+
+class ModelSearchQueryBuilder {}
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/SearchQueryBuilder.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/SearchQueryBuilder.scala
index 014d29e0e7..0f40f28932 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/SearchQueryBuilder.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/SearchQueryBuilder.scala
@@ -36,6 +36,7 @@ object SearchQueryBuilder {
   val WORKFLOW_RESOURCE_TYPE = "workflow"
   val PROJECT_RESOURCE_TYPE = "project"
   val DATASET_RESOURCE_TYPE = "dataset"
+  val MODEL_RESOURCE_TYPE = "model"
   val ALL_RESOURCE_TYPE = ""
 }
 
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/UnifiedResourceSchema.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/UnifiedResourceSchema.scala
index 5d2c52584b..894b688943 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/UnifiedResourceSchema.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/UnifiedResourceSchema.scala
@@ -67,9 +67,13 @@ object UnifiedResourceSchema {
       projectOwnerId: Field[Integer] = DSL.cast(null, classOf[Integer]),
       projectColor: Field[String] = DSL.inline(""),
       did: Field[Integer] = DSL.cast(null, classOf[Integer]),
+      mid: Field[Integer] = DSL.cast(null, classOf[Integer]),
       datasetStoragePath: Field[String] = DSL.cast(null, classOf[String]),
       isDatasetPublic: Field[java.lang.Boolean] = DSL.cast(null, classOf[java.lang.Boolean]),
-      datasetUserAccess: Field[PrivilegeEnum] = DSL.castNull(classOf[PrivilegeEnum])
+      datasetUserAccess: Field[PrivilegeEnum] = DSL.castNull(classOf[PrivilegeEnum]),
+      modelStoragePath: Field[String] = DSL.cast(null, classOf[String]),
+  isModelPublic: Field[java.lang.Boolean] = DSL.cast(null, classOf[java.lang.Boolean]),
+      modelUserAccess: Field[PrivilegeEnum] = DSL.castNull(classOf[PrivilegeEnum])
   ): UnifiedResourceSchema = {
     new UnifiedResourceSchema(
       Seq(
@@ -89,6 +93,10 @@ object UnifiedResourceSchema {
         projectOwnerId -> projectOwnerId.as("owner_uid"),
         projectColor -> projectColor.as("color"),
         did -> did.as("did"),
+        mid -> mid.as("mid"),
+        modelStoragePath -> modelStoragePath.as("model_storage_path"),
+        modelUserAccess -> modelUserAccess.as("user_model_access"),
+        isModelPublic -> isModelPublic.as("is_model_public"),
         datasetStoragePath -> datasetStoragePath.as("dataset_storage_path"),
         datasetUserAccess -> datasetUserAccess.as("user_dataset_access"),
         isDatasetPublic -> isDatasetPublic.as("is_dataset_public")
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/EntityTables.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/EntityTables.scala
index e6a8a8d813..4aeedd28fd 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/EntityTables.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/EntityTables.scala
@@ -83,10 +83,19 @@ object EntityTables {
       override val idColumn: TableField[DatasetUserLikesRecord, Integer] = DATASET_USER_LIKES.DID
     }
 
+    case object ModelLikeTable extends LikeTable {
+      override type R = ModelUserLikesRecord
+      override val table: Table[ModelUserLikesRecord] = MODEL_USER_LIKES
+      override val uidColumn: TableField[ModelUserLikesRecord, Integer] =
+        MODEL_USER_LIKES.UID
+      override val idColumn: TableField[ModelUserLikesRecord, Integer] = MODEL_USER_LIKES.MID
+    }
+
     def apply(entityType: EntityType): LikeTable =
       entityType match {
         case EntityType.Workflow => WorkflowLikeTable
         case EntityType.Dataset  => DatasetLikeTable
+        case EntityType.Model  => ModelLikeTable
       }
   }
 
@@ -135,11 +144,19 @@ object EntityTables {
       override val viewCountColumn: TableField[DatasetViewCountRecord, Integer] =
         DATASET_VIEW_COUNT.VIEW_COUNT
     }
+    case object ModelViewCountTable extends ViewCountTable {
+      override type R = ModelViewCountRecord
+      override val table: Table[ModelViewCountRecord] = MODEL_VIEW_COUNT
+      override val idColumn: TableField[ModelViewCountRecord, Integer] = MODEL_VIEW_COUNT.MID
+      override val viewCountColumn: TableField[ModelViewCountRecord, Integer] =
+        MODEL_VIEW_COUNT.VIEW_COUNT
+    }
 
     def apply(entityType: EntityType): ViewCountTable =
       entityType match {
         case EntityType.Workflow => WorkflowViewCountTable
         case EntityType.Dataset  => DatasetViewCountTable
+        case EntityType.Model  => ModelViewCountTable
       }
   }
 }
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/EntityType.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/EntityType.scala
index 0ef787716b..46e9bc17e5 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/EntityType.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/EntityType.scala
@@ -35,8 +35,8 @@ sealed trait EntityType {
 object EntityType {
   case object Workflow extends EntityType { val value = "workflow" }
   case object Dataset extends EntityType { val value = "dataset" }
-
-  private val values = Seq(Workflow, Dataset)
+  case object Model extends EntityType { val value = "model" }
+  private val values = Seq(Workflow, Dataset, Model)
 
   @JsonCreator
   def fromString(s: String): EntityType =
diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/HubResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/HubResource.scala
index 84c5c9f028..6c803d2f5e 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/HubResource.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/HubResource.scala
@@ -21,23 +21,8 @@ package edu.uci.ics.texera.web.resource.dashboard.hub
 
 import edu.uci.ics.texera.dao.SqlServer
 import edu.uci.ics.texera.dao.jooq.generated.Tables._
-import HubResource.{
-  AccessResponse,
-  CountResponse,
-  LikedResponse,
-  UserRequest,
-  ViewRequest,
-  fetchDashboardDatasetsByDids,
-  fetchDashboardWorkflowsByWids,
-  isLikedHelper,
-  recordLikeActivity,
-  recordUserActivity
-}
-import edu.uci.ics.texera.web.resource.dashboard.user.workflow.WorkflowResource.{
-  DashboardWorkflow,
-  baseWorkflowSelect,
-  mapWorkflowEntries
-}
+import HubResource.{AccessResponse, CountResponse, LikedResponse, UserRequest, ViewRequest, fetchDashboardDatasetsByDids, fetchDashboardWorkflowsByWids, isLikedHelper, recordLikeActivity, recordUserActivity}
+import edu.uci.ics.texera.web.resource.dashboard.user.workflow.WorkflowResource.{DashboardWorkflow, baseWorkflowSelect, mapWorkflowEntries}
 import org.jooq.impl.DSL
 
 import java.util.regex.Pattern
@@ -55,6 +40,7 @@ import edu.uci.ics.texera.dao.jooq.generated.tables.User.USER
 import edu.uci.ics.texera.dao.jooq.generated.tables.pojos.{Dataset, DatasetUserAccess}
 import edu.uci.ics.texera.web.resource.dashboard.DashboardResource.DashboardClickableFileEntry
 import edu.uci.ics.texera.web.resource.dashboard.hub.ActionType.{Clone, Like, Unlike, View}
+import edu.uci.ics.texera.web.resource.dashboard.hub.EntityType.Model
 import edu.uci.ics.texera.web.resource.dashboard.user.dataset.DatasetResource.DashboardDataset
 import io.dropwizard.auth.Auth
 import org.jooq.Table
@@ -614,7 +600,7 @@ class HubResource {
           } else Map.empty
 
         val cloneMap: Map[Int, Int] =
-          if (requestedActions.contains(ActionType.Clone) && etype != EntityType.Dataset) {
+          if (requestedActions.contains(ActionType.Clone) && etype != EntityType.Dataset && etype !=EntityType.Model) {
             val cloneTbl = CloneTable(etype)
             context
               .select(cloneTbl.idColumn, DSL.count().`as`("cnt"))
@@ -679,6 +665,8 @@ class HubResource {
             (WORKFLOW_USER_ACCESS: Table[_], WORKFLOW_USER_ACCESS.WID, WORKFLOW_USER_ACCESS.UID)
           case EntityType.Dataset =>
             (DATASET_USER_ACCESS: Table[_], DATASET_USER_ACCESS.DID, DATASET_USER_ACCESS.UID)
+          case EntityType.Model =>
+            (MODEL_USER_ACCESS: Table[_], MODEL_USER_ACCESS.MID, MODEL_USER_ACCESS.UID)
         }
 
         val records = context
diff --git a/core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/model/ModelResource.scala
similarity index 65%
copy from core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts
copy to core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/model/ModelResource.scala
index 2ea0058264..3fd4efe5bd 100644
--- a/core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/model/ModelResource.scala
@@ -1,4 +1,4 @@
-/**
+/*
  * 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
@@ -17,13 +17,18 @@
  * under the License.
  */
 
-import { Dataset, DatasetVersion } from "../../common/type/dataset";
-import { DatasetFileNode } from "../../common/type/datasetVersionFileTree";
+package edu.uci.ics.texera.web.resource.dashboard.user.model
 
-export interface DashboardDataset {
-  isOwner: boolean;
-  ownerEmail: string;
-  dataset: Dataset;
-  accessPrivilege: "READ" | "WRITE" | "NONE";
-  size: number;
+import edu.uci.ics.texera.dao.jooq.generated.tables.pojos.Model
+import org.jooq.EnumType
+
+object ModelResource {
+  // TODO: move these community resource definitions to a centralized package, similar to workflow-core
+  case class DashboardModel(
+      model: Model,
+      ownerEmail: String,
+      accessPrivilege: EnumType,
+      isOwner: Boolean,
+      size: Long
+  )
 }
diff --git a/core/config/src/main/resources/default.conf b/core/config/src/main/resources/default.conf
index fa6e949c1d..8506dd7f0d 100644
--- a/core/config/src/main/resources/default.conf
+++ b/core/config/src/main/resources/default.conf
@@ -43,6 +43,7 @@ gui {
     projects_enabled = false
     workflows_enabled = true
     datasets_enabled = true
+    models_enabled = true
     quota_enabled = true
     forum_enabled = false
 
diff --git a/core/config/src/main/resources/storage.conf b/core/config/src/main/resources/storage.conf
index 276c1efad8..b5c8395299 100644
--- a/core/config/src/main/resources/storage.conf
+++ b/core/config/src/main/resources/storage.conf
@@ -122,7 +122,7 @@ storage {
 
     # Configuration for Postgres, used for user system data & metadata storage
     jdbc {
-        url = "jdbc:postgresql://localhost:5432/texera_db?currentSchema=texera_db,public"
+        url = "jdbc:postgresql://localhost:5432/postgres?currentSchema=texera_db"
         url = ${?STORAGE_JDBC_URL}
 
         username = "postgres"
diff --git a/core/config/src/main/resources/user-system.conf b/core/config/src/main/resources/user-system.conf
index aa7d65a333..c9cd480b33 100644
--- a/core/config/src/main/resources/user-system.conf
+++ b/core/config/src/main/resources/user-system.conf
@@ -17,7 +17,7 @@
 
 # See PR https://github.com/Texera/texera/pull/3326 for configuration guidelines.
 user-sys {
-  enabled = false
+  enabled = true
   enabled = ${?USER_SYS_ENABLED}
 
   admin-username = "texera"
@@ -39,7 +39,7 @@ user-sys {
     }
   }
 
-  domain = ""
+  domain = "texera"
   domain = ${?USER_SYS_DOMAIN}
 
   invite-only = false
diff --git a/core/dao/src/main/scala/edu/uci/ics/texera/dao/SqlServer.scala b/core/dao/src/main/scala/edu/uci/ics/texera/dao/SqlServer.scala
index 18eb92b9f3..8973aa2737 100644
--- a/core/dao/src/main/scala/edu/uci/ics/texera/dao/SqlServer.scala
+++ b/core/dao/src/main/scala/edu/uci/ics/texera/dao/SqlServer.scala
@@ -52,6 +52,8 @@ object SqlServer {
   private var instance: Option[SqlServer] = None
 
   def initConnection(url: String, user: String, password: String): Unit = {
+
+    println(s"Initializing SqlServer connection with URL: $url, User: $user, Password: $password")
     if (instance.isEmpty) {
       val server = new SqlServer(url, user, password)
       instance = Some(server)
diff --git a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/Keys.java b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/Keys.java
index b07305f249..7c134084f9 100644
--- a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/Keys.java
+++ b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/Keys.java
@@ -27,6 +27,11 @@ import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetUserAccess;
 import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetUserLikes;
 import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetVersion;
 import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetViewCount;
+import edu.uci.ics.texera.dao.jooq.generated.tables.Model;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelUserAccess;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelUserLikes;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelVersion;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelViewCount;
 import edu.uci.ics.texera.dao.jooq.generated.tables.OperatorExecutions;
 import edu.uci.ics.texera.dao.jooq.generated.tables.OperatorPortExecutions;
 import edu.uci.ics.texera.dao.jooq.generated.tables.Project;
@@ -52,6 +57,11 @@ import edu.uci.ics.texera.dao.jooq.generated.tables.records.DatasetUserAccessRec
 import edu.uci.ics.texera.dao.jooq.generated.tables.records.DatasetUserLikesRecord;
 import edu.uci.ics.texera.dao.jooq.generated.tables.records.DatasetVersionRecord;
 import edu.uci.ics.texera.dao.jooq.generated.tables.records.DatasetViewCountRecord;
+import edu.uci.ics.texera.dao.jooq.generated.tables.records.ModelRecord;
+import edu.uci.ics.texera.dao.jooq.generated.tables.records.ModelUserAccessRecord;
+import edu.uci.ics.texera.dao.jooq.generated.tables.records.ModelUserLikesRecord;
+import edu.uci.ics.texera.dao.jooq.generated.tables.records.ModelVersionRecord;
+import edu.uci.ics.texera.dao.jooq.generated.tables.records.ModelViewCountRecord;
 import edu.uci.ics.texera.dao.jooq.generated.tables.records.OperatorExecutionsRecord;
 import edu.uci.ics.texera.dao.jooq.generated.tables.records.OperatorPortExecutionsRecord;
 import edu.uci.ics.texera.dao.jooq.generated.tables.records.ProjectRecord;
@@ -96,6 +106,11 @@ public class Keys {
     public static final UniqueKey<DatasetUserLikesRecord> DATASET_USER_LIKES_PKEY = Internal.createUniqueKey(DatasetUserLikes.DATASET_USER_LIKES, DSL.name("dataset_user_likes_pkey"), new TableField[] { DatasetUserLikes.DATASET_USER_LIKES.UID, DatasetUserLikes.DATASET_USER_LIKES.DID }, true);
     public static final UniqueKey<DatasetVersionRecord> DATASET_VERSION_PKEY = Internal.createUniqueKey(DatasetVersion.DATASET_VERSION, DSL.name("dataset_version_pkey"), new TableField[] { DatasetVersion.DATASET_VERSION.DVID }, true);
     public static final UniqueKey<DatasetViewCountRecord> DATASET_VIEW_COUNT_PKEY = Internal.createUniqueKey(DatasetViewCount.DATASET_VIEW_COUNT, DSL.name("dataset_view_count_pkey"), new TableField[] { DatasetViewCount.DATASET_VIEW_COUNT.DID }, true);
+    public static final UniqueKey<ModelRecord> MODEL_PKEY = Internal.createUniqueKey(Model.MODEL, DSL.name("model_pkey"), new TableField[] { Model.MODEL.MID }, true);
+    public static final UniqueKey<ModelUserAccessRecord> MODEL_USER_ACCESS_PKEY = Internal.createUniqueKey(ModelUserAccess.MODEL_USER_ACCESS, DSL.name("model_user_access_pkey"), new TableField[] { ModelUserAccess.MODEL_USER_ACCESS.MID, ModelUserAccess.MODEL_USER_ACCESS.UID }, true);
+    public static final UniqueKey<ModelUserLikesRecord> MODEL_USER_LIKES_PKEY = Internal.createUniqueKey(ModelUserLikes.MODEL_USER_LIKES, DSL.name("model_user_likes_pkey"), new TableField[] { ModelUserLikes.MODEL_USER_LIKES.UID, ModelUserLikes.MODEL_USER_LIKES.MID }, true);
+    public static final UniqueKey<ModelVersionRecord> MODEL_VERSION_PKEY = Internal.createUniqueKey(ModelVersion.MODEL_VERSION, DSL.name("model_version_pkey"), new TableField[] { ModelVersion.MODEL_VERSION.MVID }, true);
+    public static final UniqueKey<ModelViewCountRecord> MODEL_VIEW_COUNT_PKEY = Internal.createUniqueKey(ModelViewCount.MODEL_VIEW_COUNT, DSL.name("model_view_count_pkey"), new TableField[] { ModelViewCount.MODEL_VIEW_COUNT.MID }, true);
     public static final UniqueKey<OperatorExecutionsRecord> OPERATOR_EXECUTIONS_PKEY = Internal.createUniqueKey(OperatorExecutions.OPERATOR_EXECUTIONS, DSL.name("operator_executions_pkey"), new TableField[] { OperatorExecutions.OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID, OperatorExecutions.OPERATOR_EXECUTIONS.OPERATOR_ID }, true);
     public static final UniqueKey<OperatorPortExecutionsRecord> OPERATOR_PORT_EXECUTIONS_PKEY = Internal.createUniqueKey(OperatorPortExecutions.OPERATOR_PORT_EXECUTIONS, DSL.name("operator_port_executions_pkey"), new TableField[] { OperatorPortExecutions.OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID, OperatorPortExecutions.OPERATOR_PORT_EXECUTIONS.GLOBAL_PORT_ID }, true);
     public static final UniqueKey<ProjectRecord> PROJECT_OWNER_ID_NAME_KEY = Internal.createUniqueKey(Project.PROJECT, DSL.name("project_owner_id_name_key"), new TableField[] { Project.PROJECT.OWNER_ID, Project.PROJECT.NAME }, true);
@@ -132,6 +147,13 @@ public class Keys {
     public static final ForeignKey<DatasetUserLikesRecord, UserRecord> DATASET_USER_LIKES__DATASET_USER_LIKES_UID_FKEY = Internal.createForeignKey(DatasetUserLikes.DATASET_USER_LIKES, DSL.name("dataset_user_likes_uid_fkey"), new TableField[] { DatasetUserLikes.DATASET_USER_LIKES.UID }, Keys.USER_PKEY, new TableField[] { User.USER.UID }, true);
     public static final ForeignKey<DatasetVersionRecord, DatasetRecord> DATASET_VERSION__DATASET_VERSION_DID_FKEY = Internal.createForeignKey(DatasetVersion.DATASET_VERSION, DSL.name("dataset_version_did_fkey"), new TableField[] { DatasetVersion.DATASET_VERSION.DID }, Keys.DATASET_PKEY, new TableField[] { Dataset.DATASET.DID }, true);
     public static final ForeignKey<DatasetViewCountRecord, DatasetRecord> DATASET_VIEW_COUNT__DATASET_VIEW_COUNT_DID_FKEY = Internal.createForeignKey(DatasetViewCount.DATASET_VIEW_COUNT, DSL.name("dataset_view_count_did_fkey"), new TableField[] { DatasetViewCount.DATASET_VIEW_COUNT.DID }, Keys.DATASET_PKEY, new TableField[] { Dataset.DATASET.DID }, true);
+    public static final ForeignKey<ModelRecord, UserRecord> MODEL__MODEL_OWNER_UID_FKEY = Internal.createForeignKey(Model.MODEL, DSL.name("model_owner_uid_fkey"), new TableField[] { Model.MODEL.OWNER_UID }, Keys.USER_PKEY, new TableField[] { User.USER.UID }, true);
+    public static final ForeignKey<ModelUserAccessRecord, ModelRecord> MODEL_USER_ACCESS__MODEL_USER_ACCESS_MID_FKEY = Internal.createForeignKey(ModelUserAccess.MODEL_USER_ACCESS, DSL.name("model_user_access_mid_fkey"), new TableField[] { ModelUserAccess.MODEL_USER_ACCESS.MID }, Keys.MODEL_PKEY, new TableField[] { Model.MODEL.MID }, true);
+    public static final ForeignKey<ModelUserAccessRecord, UserRecord> MODEL_USER_ACCESS__MODEL_USER_ACCESS_UID_FKEY = Internal.createForeignKey(ModelUserAccess.MODEL_USER_ACCESS, DSL.name("model_user_access_uid_fkey"), new TableField[] { ModelUserAccess.MODEL_USER_ACCESS.UID }, Keys.USER_PKEY, new TableField[] { User.USER.UID }, true);
+    public static final ForeignKey<ModelUserLikesRecord, ModelRecord> MODEL_USER_LIKES__MODEL_USER_LIKES_MID_FKEY = Internal.createForeignKey(ModelUserLikes.MODEL_USER_LIKES, DSL.name("model_user_likes_mid_fkey"), new TableField[] { ModelUserLikes.MODEL_USER_LIKES.MID }, Keys.MODEL_PKEY, new TableField[] { Model.MODEL.MID }, true);
+    public static final ForeignKey<ModelUserLikesRecord, UserRecord> MODEL_USER_LIKES__MODEL_USER_LIKES_UID_FKEY = Internal.createForeignKey(ModelUserLikes.MODEL_USER_LIKES, DSL.name("model_user_likes_uid_fkey"), new TableField[] { ModelUserLikes.MODEL_USER_LIKES.UID }, Keys.USER_PKEY, new TableField[] { User.USER.UID }, true);
+    public static final ForeignKey<ModelVersionRecord, ModelRecord> MODEL_VERSION__MODEL_VERSION_MID_FKEY = Internal.createForeignKey(ModelVersion.MODEL_VERSION, DSL.name("model_version_mid_fkey"), new TableField[] { ModelVersion.MODEL_VERSION.MID }, Keys.MODEL_PKEY, new TableField[] { Model.MODEL.MID }, true);
+    public static final ForeignKey<ModelViewCountRecord, ModelRecord> MODEL_VIEW_COUNT__MODEL_VIEW_COUNT_MID_FKEY = Internal.createForeignKey(ModelViewCount.MODEL_VIEW_COUNT, DSL.name("model_view_count_mid_fkey"), new TableField[] { ModelViewCount.MODEL_VIEW_COUNT.MID }, Keys.MODEL_PKEY, new TableField[] { Model.MODEL.MID }, true);
     public static final ForeignKey<OperatorExecutionsRecord, WorkflowExecutionsRecord> OPERATOR_EXECUTIONS__OPERATOR_EXECUTIONS_WORKFLOW_EXECUTION_ID_FKEY = Internal.createForeignKey(OperatorExecutions.OPERATOR_EXECUTIONS, DSL.name("operator_executions_workflow_execution_id_fkey"), new TableField[] { OperatorExecutions.OPERATOR_EXECUTIONS.WORKFLOW_EXECUTION_ID }, Keys.WORKFLOW_EXECUTIONS_PKEY, new TableField[] { WorkflowExecutions.WORKFLOW_EXECUTIONS.EID }, true);
     public static final ForeignKey<OperatorPortExecutionsRecord, WorkflowExecutionsRecord> OPERATOR_PORT_EXECUTIONS__OPERATOR_PORT_EXECUTIONS_WORKFLOW_EXECUTION_ID_FKEY = Internal.createForeignKey(OperatorPortExecutions.OPERATOR_PORT_EXECUTIONS, DSL.name("operator_port_executions_workflow_execution_id_fkey"), new TableField[] { OperatorPortExecutions.OPERATOR_PORT_EXECUTIONS.WORKFLOW_EXECUTION_ID }, Keys.WORKFLOW_EXECUTIONS_PKEY, new TableField[] { WorkflowExecutions.WORKFLOW_EXECUTIONS. [...]
     public static final ForeignKey<ProjectRecord, UserRecord> PROJECT__PROJECT_OWNER_ID_FKEY = Internal.createForeignKey(Project.PROJECT, DSL.name("project_owner_id_fkey"), new TableField[] { Project.PROJECT.OWNER_ID }, Keys.USER_PKEY, new TableField[] { User.USER.UID }, true);
diff --git a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/Tables.java b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/Tables.java
index 39af6f8f8f..19024a784d 100644
--- a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/Tables.java
+++ b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/Tables.java
@@ -27,6 +27,11 @@ import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetUserAccess;
 import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetUserLikes;
 import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetVersion;
 import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetViewCount;
+import edu.uci.ics.texera.dao.jooq.generated.tables.Model;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelUserAccess;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelUserLikes;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelVersion;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelViewCount;
 import edu.uci.ics.texera.dao.jooq.generated.tables.OperatorExecutions;
 import edu.uci.ics.texera.dao.jooq.generated.tables.OperatorPortExecutions;
 import edu.uci.ics.texera.dao.jooq.generated.tables.Project;
@@ -85,6 +90,31 @@ public class Tables {
      */
     public static final DatasetViewCount DATASET_VIEW_COUNT = DatasetViewCount.DATASET_VIEW_COUNT;
 
+    /**
+     * The table <code>texera_db.model</code>.
+     */
+    public static final Model MODEL = Model.MODEL;
+
+    /**
+     * The table <code>texera_db.model_user_access</code>.
+     */
+    public static final ModelUserAccess MODEL_USER_ACCESS = ModelUserAccess.MODEL_USER_ACCESS;
+
+    /**
+     * The table <code>texera_db.model_user_likes</code>.
+     */
+    public static final ModelUserLikes MODEL_USER_LIKES = ModelUserLikes.MODEL_USER_LIKES;
+
+    /**
+     * The table <code>texera_db.model_version</code>.
+     */
+    public static final ModelVersion MODEL_VERSION = ModelVersion.MODEL_VERSION;
+
+    /**
+     * The table <code>texera_db.model_view_count</code>.
+     */
+    public static final ModelViewCount MODEL_VIEW_COUNT = ModelViewCount.MODEL_VIEW_COUNT;
+
     /**
      * The table <code>texera_db.operator_executions</code>.
      */
diff --git a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/TexeraDb.java b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/TexeraDb.java
index 269aad41f3..dba2a1f0ec 100644
--- a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/TexeraDb.java
+++ b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/TexeraDb.java
@@ -27,6 +27,11 @@ import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetUserAccess;
 import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetUserLikes;
 import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetVersion;
 import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetViewCount;
+import edu.uci.ics.texera.dao.jooq.generated.tables.Model;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelUserAccess;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelUserLikes;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelVersion;
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelViewCount;
 import edu.uci.ics.texera.dao.jooq.generated.tables.OperatorExecutions;
 import edu.uci.ics.texera.dao.jooq.generated.tables.OperatorPortExecutions;
 import edu.uci.ics.texera.dao.jooq.generated.tables.Project;
@@ -99,6 +104,31 @@ public class TexeraDb extends SchemaImpl {
      */
     public final DatasetViewCount DATASET_VIEW_COUNT = DatasetViewCount.DATASET_VIEW_COUNT;
 
+    /**
+     * The table <code>texera_db.model</code>.
+     */
+    public final Model MODEL = Model.MODEL;
+
+    /**
+     * The table <code>texera_db.model_user_access</code>.
+     */
+    public final ModelUserAccess MODEL_USER_ACCESS = ModelUserAccess.MODEL_USER_ACCESS;
+
+    /**
+     * The table <code>texera_db.model_user_likes</code>.
+     */
+    public final ModelUserLikes MODEL_USER_LIKES = ModelUserLikes.MODEL_USER_LIKES;
+
+    /**
+     * The table <code>texera_db.model_version</code>.
+     */
+    public final ModelVersion MODEL_VERSION = ModelVersion.MODEL_VERSION;
+
+    /**
+     * The table <code>texera_db.model_view_count</code>.
+     */
+    public final ModelViewCount MODEL_VIEW_COUNT = ModelViewCount.MODEL_VIEW_COUNT;
+
     /**
      * The table <code>texera_db.operator_executions</code>.
      */
@@ -221,6 +251,11 @@ public class TexeraDb extends SchemaImpl {
             DatasetUserLikes.DATASET_USER_LIKES,
             DatasetVersion.DATASET_VERSION,
             DatasetViewCount.DATASET_VIEW_COUNT,
+            Model.MODEL,
+            ModelUserAccess.MODEL_USER_ACCESS,
+            ModelUserLikes.MODEL_USER_LIKES,
+            ModelVersion.MODEL_VERSION,
+            ModelViewCount.MODEL_VIEW_COUNT,
             OperatorExecutions.OPERATOR_EXECUTIONS,
             OperatorPortExecutions.OPERATOR_PORT_EXECUTIONS,
             Project.PROJECT,
diff --git a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/Dataset.java b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/Dataset.java
index fe6283a03f..084562e0fa 100644
--- a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/Dataset.java
+++ b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/Dataset.java
@@ -86,6 +86,11 @@ public class Dataset extends TableImpl<DatasetRecord> {
      */
     public final TableField<DatasetRecord, Boolean> IS_PUBLIC = createField(DSL.name("is_public"), SQLDataType.BOOLEAN.nullable(false).defaultValue(DSL.field("true", SQLDataType.BOOLEAN)), this, "");
 
+    /**
+     * The column <code>texera_db.dataset.is_downloadable</code>.
+     */
+    public final TableField<DatasetRecord, Boolean> IS_DOWNLOADABLE = createField(DSL.name("is_downloadable"), SQLDataType.BOOLEAN.nullable(false).defaultValue(DSL.field("true", SQLDataType.BOOLEAN)), this, "");
+
     /**
      * The column <code>texera_db.dataset.description</code>.
      */
@@ -96,11 +101,6 @@ public class Dataset extends TableImpl<DatasetRecord> {
      */
     public final TableField<DatasetRecord, Timestamp> CREATION_TIME = createField(DSL.name("creation_time"), SQLDataType.TIMESTAMP(0).nullable(false).defaultValue(DSL.field("CURRENT_TIMESTAMP", SQLDataType.TIMESTAMP)), this, "");
 
-    /**
-     * The column <code>texera_db.dataset.is_downloadable</code>.
-     */
-    public final TableField<DatasetRecord, Boolean> IS_DOWNLOADABLE = createField(DSL.name("is_downloadable"), SQLDataType.BOOLEAN.nullable(false).defaultValue(DSL.field("true", SQLDataType.BOOLEAN)), this, "");
-
     private Dataset(Name alias, Table<DatasetRecord> aliased) {
         this(alias, aliased, null);
     }
@@ -197,7 +197,7 @@ public class Dataset extends TableImpl<DatasetRecord> {
     // -------------------------------------------------------------------------
 
     @Override
-    public Row7<Integer, Integer, String, Boolean, String, Timestamp, Boolean> fieldsRow() {
+    public Row7<Integer, Integer, String, Boolean, Boolean, String, Timestamp> fieldsRow() {
         return (Row7) super.fieldsRow();
     }
 }
diff --git a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/daos/DatasetDao.java b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/daos/DatasetDao.java
index 2a03a24415..124c88df3b 100644
--- a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/daos/DatasetDao.java
+++ b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/daos/DatasetDao.java
@@ -131,6 +131,21 @@ public class DatasetDao extends DAOImpl<DatasetRecord, edu.uci.ics.texera.dao.jo
         return fetch(Dataset.DATASET.IS_PUBLIC, values);
     }
 
+    /**
+     * Fetch records that have <code>is_downloadable BETWEEN lowerInclusive AND
+     * upperInclusive</code>
+     */
+    public List<edu.uci.ics.texera.dao.jooq.generated.tables.pojos.Dataset> fetchRangeOfIsDownloadable(Boolean lowerInclusive, Boolean upperInclusive) {
+        return fetchRange(Dataset.DATASET.IS_DOWNLOADABLE, lowerInclusive, upperInclusive);
+    }
+
+    /**
+     * Fetch records that have <code>is_downloadable IN (values)</code>
+     */
+    public List<edu.uci.ics.texera.dao.jooq.generated.tables.pojos.Dataset> fetchByIsDownloadable(Boolean... values) {
+        return fetch(Dataset.DATASET.IS_DOWNLOADABLE, values);
+    }
+
     /**
      * Fetch records that have <code>description BETWEEN lowerInclusive AND
      * upperInclusive</code>
@@ -160,19 +175,4 @@ public class DatasetDao extends DAOImpl<DatasetRecord, edu.uci.ics.texera.dao.jo
     public List<edu.uci.ics.texera.dao.jooq.generated.tables.pojos.Dataset> fetchByCreationTime(Timestamp... values) {
         return fetch(Dataset.DATASET.CREATION_TIME, values);
     }
-
-    /**
-     * Fetch records that have <code>is_downloadable BETWEEN lowerInclusive AND
-     * upperInclusive</code>
-     */
-    public List<edu.uci.ics.texera.dao.jooq.generated.tables.pojos.Dataset> fetchRangeOfIsDownloadable(Boolean lowerInclusive, Boolean upperInclusive) {
-        return fetchRange(Dataset.DATASET.IS_DOWNLOADABLE, lowerInclusive, upperInclusive);
-    }
-
-    /**
-     * Fetch records that have <code>is_downloadable IN (values)</code>
-     */
-    public List<edu.uci.ics.texera.dao.jooq.generated.tables.pojos.Dataset> fetchByIsDownloadable(Boolean... values) {
-        return fetch(Dataset.DATASET.IS_DOWNLOADABLE, values);
-    }
 }
diff --git a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/interfaces/IDataset.java b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/interfaces/IDataset.java
index cf07efa094..3396aefbc2 100644
--- a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/interfaces/IDataset.java
+++ b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/interfaces/IDataset.java
@@ -71,6 +71,16 @@ public interface IDataset extends Serializable {
      */
     public Boolean getIsPublic();
 
+    /**
+     * Setter for <code>texera_db.dataset.is_downloadable</code>.
+     */
+    public void setIsDownloadable(Boolean value);
+
+    /**
+     * Getter for <code>texera_db.dataset.is_downloadable</code>.
+     */
+    public Boolean getIsDownloadable();
+
     /**
      * Setter for <code>texera_db.dataset.description</code>.
      */
@@ -91,16 +101,6 @@ public interface IDataset extends Serializable {
      */
     public Timestamp getCreationTime();
 
-    /**
-     * Setter for <code>texera_db.dataset.is_downloadable</code>.
-     */
-    public void setIsDownloadable(Boolean value);
-
-    /**
-     * Getter for <code>texera_db.dataset.is_downloadable</code>.
-     */
-    public Boolean getIsDownloadable();
-
     // -------------------------------------------------------------------------
     // FROM and INTO
     // -------------------------------------------------------------------------
diff --git a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/pojos/Dataset.java b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/pojos/Dataset.java
index 15be7dace4..ac2e72561e 100644
--- a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/pojos/Dataset.java
+++ b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/pojos/Dataset.java
@@ -38,9 +38,9 @@ public class Dataset implements IDataset {
     private Integer   ownerUid;
     private String    name;
     private Boolean   isPublic;
+    private Boolean   isDownloadable;
     private String    description;
     private Timestamp creationTime;
-    private Boolean   isDownloadable;
 
     public Dataset() {}
 
@@ -49,9 +49,9 @@ public class Dataset implements IDataset {
         this.ownerUid = value.getOwnerUid();
         this.name = value.getName();
         this.isPublic = value.getIsPublic();
+        this.isDownloadable = value.getIsDownloadable();
         this.description = value.getDescription();
         this.creationTime = value.getCreationTime();
-        this.isDownloadable = value.getIsDownloadable();
     }
 
     public Dataset(
@@ -59,17 +59,17 @@ public class Dataset implements IDataset {
         Integer   ownerUid,
         String    name,
         Boolean   isPublic,
+        Boolean   isDownloadable,
         String    description,
-        Timestamp creationTime,
-        Boolean   isDownloadable
+        Timestamp creationTime
     ) {
         this.did = did;
         this.ownerUid = ownerUid;
         this.name = name;
         this.isPublic = isPublic;
+        this.isDownloadable = isDownloadable;
         this.description = description;
         this.creationTime = creationTime;
-        this.isDownloadable = isDownloadable;
     }
 
     /**
@@ -136,6 +136,22 @@ public class Dataset implements IDataset {
         this.isPublic = isPublic;
     }
 
+    /**
+     * Getter for <code>texera_db.dataset.is_downloadable</code>.
+     */
+    @Override
+    public Boolean getIsDownloadable() {
+        return this.isDownloadable;
+    }
+
+    /**
+     * Setter for <code>texera_db.dataset.is_downloadable</code>.
+     */
+    @Override
+    public void setIsDownloadable(Boolean isDownloadable) {
+        this.isDownloadable = isDownloadable;
+    }
+
     /**
      * Getter for <code>texera_db.dataset.description</code>.
      */
@@ -168,22 +184,6 @@ public class Dataset implements IDataset {
         this.creationTime = creationTime;
     }
 
-    /**
-     * Getter for <code>texera_db.dataset.is_downloadable</code>.
-     */
-    @Override
-    public Boolean getIsDownloadable() {
-        return this.isDownloadable;
-    }
-
-    /**
-     * Setter for <code>texera_db.dataset.is_downloadable</code>.
-     */
-    @Override
-    public void setIsDownloadable(Boolean isDownloadable) {
-        this.isDownloadable = isDownloadable;
-    }
-
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder("Dataset (");
@@ -192,9 +192,9 @@ public class Dataset implements IDataset {
         sb.append(", ").append(ownerUid);
         sb.append(", ").append(name);
         sb.append(", ").append(isPublic);
+        sb.append(", ").append(isDownloadable);
         sb.append(", ").append(description);
         sb.append(", ").append(creationTime);
-        sb.append(", ").append(isDownloadable);
 
         sb.append(")");
         return sb.toString();
@@ -210,9 +210,9 @@ public class Dataset implements IDataset {
         setOwnerUid(from.getOwnerUid());
         setName(from.getName());
         setIsPublic(from.getIsPublic());
+        setIsDownloadable(from.getIsDownloadable());
         setDescription(from.getDescription());
         setCreationTime(from.getCreationTime());
-        setIsDownloadable(from.getIsDownloadable());
     }
 
     @Override
diff --git a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/records/DatasetRecord.java b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/records/DatasetRecord.java
index 7070836def..d451864bab 100644
--- a/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/records/DatasetRecord.java
+++ b/core/dao/src/main/scala/edu/uci/ics/texera/dao/jooq/generated/tables/records/DatasetRecord.java
@@ -37,7 +37,7 @@ import org.jooq.impl.UpdatableRecordImpl;
  * This class is generated by jOOQ.
  */
 @SuppressWarnings({ "all", "unchecked", "rawtypes" })
-public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements Record7<Integer, Integer, String, Boolean, String, Timestamp, Boolean>, IDataset {
+public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements Record7<Integer, Integer, String, Boolean, Boolean, String, Timestamp>, IDataset {
 
     private static final long serialVersionUID = 1L;
 
@@ -106,51 +106,51 @@ public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements
     }
 
     /**
-     * Setter for <code>texera_db.dataset.description</code>.
+     * Setter for <code>texera_db.dataset.is_downloadable</code>.
      */
     @Override
-    public void setDescription(String value) {
+    public void setIsDownloadable(Boolean value) {
         set(4, value);
     }
 
     /**
-     * Getter for <code>texera_db.dataset.description</code>.
+     * Getter for <code>texera_db.dataset.is_downloadable</code>.
      */
     @Override
-    public String getDescription() {
-        return (String) get(4);
+    public Boolean getIsDownloadable() {
+        return (Boolean) get(4);
     }
 
     /**
-     * Setter for <code>texera_db.dataset.creation_time</code>.
+     * Setter for <code>texera_db.dataset.description</code>.
      */
     @Override
-    public void setCreationTime(Timestamp value) {
+    public void setDescription(String value) {
         set(5, value);
     }
 
     /**
-     * Getter for <code>texera_db.dataset.creation_time</code>.
+     * Getter for <code>texera_db.dataset.description</code>.
      */
     @Override
-    public Timestamp getCreationTime() {
-        return (Timestamp) get(5);
+    public String getDescription() {
+        return (String) get(5);
     }
 
     /**
-     * Setter for <code>texera_db.dataset.is_downloadable</code>.
+     * Setter for <code>texera_db.dataset.creation_time</code>.
      */
     @Override
-    public void setIsDownloadable(Boolean value) {
+    public void setCreationTime(Timestamp value) {
         set(6, value);
     }
 
     /**
-     * Getter for <code>texera_db.dataset.is_downloadable</code>.
+     * Getter for <code>texera_db.dataset.creation_time</code>.
      */
     @Override
-    public Boolean getIsDownloadable() {
-        return (Boolean) get(6);
+    public Timestamp getCreationTime() {
+        return (Timestamp) get(6);
     }
 
     // -------------------------------------------------------------------------
@@ -167,12 +167,12 @@ public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements
     // -------------------------------------------------------------------------
 
     @Override
-    public Row7<Integer, Integer, String, Boolean, String, Timestamp, Boolean> fieldsRow() {
+    public Row7<Integer, Integer, String, Boolean, Boolean, String, Timestamp> fieldsRow() {
         return (Row7) super.fieldsRow();
     }
 
     @Override
-    public Row7<Integer, Integer, String, Boolean, String, Timestamp, Boolean> valuesRow() {
+    public Row7<Integer, Integer, String, Boolean, Boolean, String, Timestamp> valuesRow() {
         return (Row7) super.valuesRow();
     }
 
@@ -197,18 +197,18 @@ public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements
     }
 
     @Override
-    public Field<String> field5() {
-        return Dataset.DATASET.DESCRIPTION;
+    public Field<Boolean> field5() {
+        return Dataset.DATASET.IS_DOWNLOADABLE;
     }
 
     @Override
-    public Field<Timestamp> field6() {
-        return Dataset.DATASET.CREATION_TIME;
+    public Field<String> field6() {
+        return Dataset.DATASET.DESCRIPTION;
     }
 
     @Override
-    public Field<Boolean> field7() {
-        return Dataset.DATASET.IS_DOWNLOADABLE;
+    public Field<Timestamp> field7() {
+        return Dataset.DATASET.CREATION_TIME;
     }
 
     @Override
@@ -232,18 +232,18 @@ public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements
     }
 
     @Override
-    public String component5() {
-        return getDescription();
+    public Boolean component5() {
+        return getIsDownloadable();
     }
 
     @Override
-    public Timestamp component6() {
-        return getCreationTime();
+    public String component6() {
+        return getDescription();
     }
 
     @Override
-    public Boolean component7() {
-        return getIsDownloadable();
+    public Timestamp component7() {
+        return getCreationTime();
     }
 
     @Override
@@ -267,18 +267,18 @@ public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements
     }
 
     @Override
-    public String value5() {
-        return getDescription();
+    public Boolean value5() {
+        return getIsDownloadable();
     }
 
     @Override
-    public Timestamp value6() {
-        return getCreationTime();
+    public String value6() {
+        return getDescription();
     }
 
     @Override
-    public Boolean value7() {
-        return getIsDownloadable();
+    public Timestamp value7() {
+        return getCreationTime();
     }
 
     @Override
@@ -306,25 +306,25 @@ public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements
     }
 
     @Override
-    public DatasetRecord value5(String value) {
-        setDescription(value);
+    public DatasetRecord value5(Boolean value) {
+        setIsDownloadable(value);
         return this;
     }
 
     @Override
-    public DatasetRecord value6(Timestamp value) {
-        setCreationTime(value);
+    public DatasetRecord value6(String value) {
+        setDescription(value);
         return this;
     }
 
     @Override
-    public DatasetRecord value7(Boolean value) {
-        setIsDownloadable(value);
+    public DatasetRecord value7(Timestamp value) {
+        setCreationTime(value);
         return this;
     }
 
     @Override
-    public DatasetRecord values(Integer value1, Integer value2, String value3, Boolean value4, String value5, Timestamp value6, Boolean value7) {
+    public DatasetRecord values(Integer value1, Integer value2, String value3, Boolean value4, Boolean value5, String value6, Timestamp value7) {
         value1(value1);
         value2(value2);
         value3(value3);
@@ -345,9 +345,9 @@ public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements
         setOwnerUid(from.getOwnerUid());
         setName(from.getName());
         setIsPublic(from.getIsPublic());
+        setIsDownloadable(from.getIsDownloadable());
         setDescription(from.getDescription());
         setCreationTime(from.getCreationTime());
-        setIsDownloadable(from.getIsDownloadable());
     }
 
     @Override
@@ -370,16 +370,16 @@ public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements
     /**
      * Create a detached, initialised DatasetRecord
      */
-    public DatasetRecord(Integer did, Integer ownerUid, String name, Boolean isPublic, String description, Timestamp creationTime, Boolean isDownloadable) {
+    public DatasetRecord(Integer did, Integer ownerUid, String name, Boolean isPublic, Boolean isDownloadable, String description, Timestamp creationTime) {
         super(Dataset.DATASET);
 
         setDid(did);
         setOwnerUid(ownerUid);
         setName(name);
         setIsPublic(isPublic);
+        setIsDownloadable(isDownloadable);
         setDescription(description);
         setCreationTime(creationTime);
-        setIsDownloadable(isDownloadable);
     }
 
     /**
@@ -393,9 +393,9 @@ public class DatasetRecord extends UpdatableRecordImpl<DatasetRecord> implements
             setOwnerUid(value.getOwnerUid());
             setName(value.getName());
             setIsPublic(value.getIsPublic());
+            setIsDownloadable(value.getIsDownloadable());
             setDescription(value.getDescription());
             setCreationTime(value.getCreationTime());
-            setIsDownloadable(value.getIsDownloadable());
         }
     }
 }
diff --git a/core/file-service/src/main/scala/edu/uci/ics/texera/service/FileService.scala b/core/file-service/src/main/scala/edu/uci/ics/texera/service/FileService.scala
index 02c2b3fc84..317e71e0c8 100644
--- a/core/file-service/src/main/scala/edu/uci/ics/texera/service/FileService.scala
+++ b/core/file-service/src/main/scala/edu/uci/ics/texera/service/FileService.scala
@@ -31,11 +31,7 @@ import edu.uci.ics.texera.auth.{JwtAuthFilter, SessionUser}
 import edu.uci.ics.texera.dao.SqlServer
 import edu.uci.ics.texera.service.`type`.DatasetFileNode
 import edu.uci.ics.texera.service.`type`.serde.DatasetFileNodeSerializer
-import edu.uci.ics.texera.service.resource.{
-  DatasetAccessResource,
-  DatasetResource,
-  HealthCheckResource
-}
+import edu.uci.ics.texera.service.resource.{DatasetAccessResource, DatasetResource, HealthCheckResource, ModelAccessResource, ModelResource}
 import edu.uci.ics.texera.service.util.S3StorageClient
 import io.dropwizard.auth.AuthDynamicFeature
 import org.eclipse.jetty.server.session.SessionHandler
@@ -80,6 +76,8 @@ class FileService extends Application[FileServiceConfiguration] with LazyLogging
 
     environment.jersey.register(classOf[DatasetResource])
     environment.jersey.register(classOf[DatasetAccessResource])
+    environment.jersey.register(classOf[ModelResource])
+    environment.jersey.register(classOf[ModelAccessResource])
   }
 }
 
diff --git a/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala b/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala
index a551b79900..ff2502838f 100644
--- a/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala
+++ b/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala
@@ -185,8 +185,8 @@ object DatasetResource {
   case class DatasetDescriptionModification(did: Integer, description: String)
 
   case class DatasetVersionRootFileNodesResponse(
-      fileNodes: List[DatasetFileNode],
-      size: Long
+                                                  fileNodes: List[DatasetFileNode],
+                                                  size: Long
   )
 }
 
@@ -289,9 +289,9 @@ class DatasetResource {
           createdDataset.getOwnerUid,
           createdDataset.getName,
           createdDataset.getIsPublic,
+          createdDataset.getIsDownloadable,
           createdDataset.getDescription,
-          createdDataset.getCreationTime,
-          createdDataset.getIsDownloadable
+          createdDataset.getCreationTime
         ),
         user.getEmail,
         PrivilegeEnum.WRITE,
diff --git a/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/ModelAccessResource.scala b/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/ModelAccessResource.scala
new file mode 100644
index 0000000000..e62d2d4cea
--- /dev/null
+++ b/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/ModelAccessResource.scala
@@ -0,0 +1,221 @@
+/*
+ * 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.
+ */
+
+package edu.uci.ics.texera.service.resource
+
+import edu.uci.ics.texera.auth.SessionUser
+import edu.uci.ics.texera.dao.SqlServer
+import edu.uci.ics.texera.dao.SqlServer.withTransaction
+import edu.uci.ics.texera.dao.jooq.generated.Tables.{MODEL_USER_ACCESS, USER}
+import edu.uci.ics.texera.dao.jooq.generated.enums.PrivilegeEnum
+import edu.uci.ics.texera.dao.jooq.generated.tables.daos.{ DatasetUserAccessDao, ModelDao, UserDao}
+import edu.uci.ics.texera.dao.jooq.generated.tables.pojos.{DatasetUserAccess, User}
+import edu.uci.ics.texera.service.resource.ModelAccessResource.{AccessEntry, context, getOwner, userHasWriteAccess}
+import io.dropwizard.auth.Auth
+import jakarta.annotation.security.RolesAllowed
+import jakarta.ws.rs.core.{MediaType, Response}
+import jakarta.ws.rs._
+import org.jooq.{DSLContext, EnumType}
+
+import javax.ws.rs.ForbiddenException
+
+object ModelAccessResource {
+  private lazy val context: DSLContext = SqlServer
+    .getInstance()
+    .createDSLContext()
+
+  def isDatasetPublic(ctx: DSLContext, mid: Integer): Boolean = {
+    val modelDao = new ModelDao(ctx.configuration())
+    Option(modelDao.fetchOneByMid(mid))
+      .flatMap(dataset => Option(dataset.getIsPublic))
+      .contains(true)
+  }
+
+  def userHasReadAccess(ctx: DSLContext, mid: Integer, uid: Integer): Boolean = {
+    isDatasetPublic(ctx, mid) ||
+    userHasWriteAccess(ctx, mid, uid) ||
+    getModelUserAccessPrivilege(ctx, mid, uid) == PrivilegeEnum.READ
+  }
+
+  def userOwnDataset(ctx: DSLContext, mid: Integer, uid: Integer): Boolean = {
+    val modelDao = new ModelDao(ctx.configuration())
+
+    Option(modelDao.fetchOneByMid(mid))
+      .exists(_.getOwnerUid == uid)
+  }
+
+  def userHasWriteAccess(ctx: DSLContext, mid: Integer, uid: Integer): Boolean = {
+    userOwnDataset(ctx, mid, uid) ||
+    getModelUserAccessPrivilege(ctx, mid, uid) == PrivilegeEnum.WRITE
+  }
+
+  def getModelUserAccessPrivilege(
+      ctx: DSLContext,
+      mid: Integer,
+      uid: Integer
+  ): PrivilegeEnum = {
+    Option(
+      ctx
+        .select(MODEL_USER_ACCESS.PRIVILEGE)
+        .from(MODEL_USER_ACCESS)
+        .where(
+          MODEL_USER_ACCESS.MID
+            .eq(mid)
+            .and(MODEL_USER_ACCESS.UID.eq(uid))
+        )
+        .fetchOneInto(classOf[PrivilegeEnum])
+    ).getOrElse(PrivilegeEnum.NONE)
+  }
+
+  def getOwner(ctx: DSLContext, mid: Integer): User = {
+    val modelDao = new ModelDao(ctx.configuration())
+    val userDao = new UserDao(ctx.configuration())
+
+    Option(modelDao.fetchOneByMid(mid))
+      .flatMap(dataset => Option(dataset.getOwnerUid))
+      .map(ownerUid => userDao.fetchOneByUid(ownerUid))
+      .orNull
+  }
+
+  case class AccessEntry(email: String, name: String, privilege: EnumType) {}
+
+}
+
+@Produces(Array(MediaType.APPLICATION_JSON))
+@RolesAllowed(Array("REGULAR", "ADMIN"))
+@Path("/access/model")
+class ModelAccessResource {
+
+  /**
+    * This method returns the owner of a dataset
+    *
+    * @param mid ,  dataset id
+    * @return ownerEmail,  the owner's email
+    */
+  @GET
+  @Path("/owner/{mid}")
+  def getOwnerEmailOfDataset(@PathParam("mid") mid: Integer): String = {
+    var email = ""
+    withTransaction(context) { ctx =>
+      val owner = getOwner(ctx, mid)
+      if (owner != null) {
+        email = owner.getEmail
+      }
+    }
+    email
+  }
+
+  /**
+    * Returns information about all current shared access of the given dataset
+    *
+    * @param mid model id
+    * @return a List of email/name/permission
+    */
+  @GET
+  @Path("/list/{mid}")
+  def getAccessList(
+      @PathParam("mid") mid: Integer
+  ): java.util.List[AccessEntry] = {
+    withTransaction(context) { ctx =>
+      val modelDao = new ModelDao(ctx.configuration())
+      ctx
+        .select(
+          USER.EMAIL,
+          USER.NAME,
+          MODEL_USER_ACCESS.PRIVILEGE
+        )
+        .from(MODEL_USER_ACCESS)
+        .join(USER)
+        .on(USER.UID.eq(MODEL_USER_ACCESS.UID))
+        .where(
+          MODEL_USER_ACCESS.MID
+            .eq(mid)
+            .and(MODEL_USER_ACCESS.UID.notEqual(modelDao.fetchOneByMid(mid).getOwnerUid))
+        )
+        .fetchInto(classOf[AccessEntry])
+    }
+  }
+
+  /**
+    * This method shares a dataset to a user with a specific access type
+    *
+    * @param mid       the given dataset
+    * @param email     the email which the access is given to
+    * @param privilege the type of Access given to the target user
+    * @return rejection if user not permitted to share the workflow or Success Message
+    */
+  @PUT
+  @Path("/grant/{mid}/{email}/{privilege}")
+  def grantAccess(
+      @PathParam("mid") mid: Integer,
+      @PathParam("email") email: String,
+      @PathParam("privilege") privilege: String,
+      @Auth user: SessionUser
+  ): Response = {
+    withTransaction(context) { ctx =>
+      if (!userHasWriteAccess(ctx, mid, user.getUid)) {
+        throw new ForbiddenException(s"You do not have permission to modify dataset $mid")
+      }
+      val datasetUserAccessDao = new DatasetUserAccessDao(ctx.configuration())
+      val userDao = new UserDao(ctx.configuration())
+      datasetUserAccessDao.merge(
+        new DatasetUserAccess(
+          mid,
+          userDao.fetchOneByEmail(email).getUid,
+          PrivilegeEnum.valueOf(privilege)
+        )
+      )
+      Response.ok().build()
+    }
+  }
+
+  /**
+    * This method revoke the user's access of the given dataset
+    *
+    * @param mid   the given dataset
+    * @param email the email of the use whose access is about to be removed
+    * @return message indicating a success message
+    */
+  @DELETE
+  @Path("/revoke/{mid}/{email}")
+  def revokeAccess(
+      @PathParam("mid") mid: Integer,
+      @PathParam("email") email: String,
+      @Auth user: SessionUser
+  ): Response = {
+    withTransaction(context) { ctx =>
+      if (!userHasWriteAccess(ctx, mid, user.getUid)) {
+        throw new ForbiddenException(s"You do not have permission to modify dataset $mid")
+      }
+
+      val userDao = new UserDao(ctx.configuration())
+
+      ctx
+        .delete(MODEL_USER_ACCESS)
+        .where(
+          MODEL_USER_ACCESS.UID
+            .eq(userDao.fetchOneByEmail(email).getUid)
+            .and(MODEL_USER_ACCESS.MID.eq(mid))
+        )
+        .execute()
+
+      Response.ok().build()
+    }
+  }
+}
diff --git a/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala b/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/ModelResource.scala
similarity index 59%
copy from core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala
copy to core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/ModelResource.scala
index a551b79900..7493796a60 100644
--- a/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/DatasetResource.scala
+++ b/core/file-service/src/main/scala/edu/uci/ics/texera/service/resource/ModelResource.scala
@@ -27,80 +27,50 @@ import edu.uci.ics.texera.auth.SessionUser
 import edu.uci.ics.texera.dao.SqlServer
 import edu.uci.ics.texera.dao.SqlServer.withTransaction
 import edu.uci.ics.texera.dao.jooq.generated.enums.PrivilegeEnum
+import edu.uci.ics.texera.dao.jooq.generated.tables.Model.MODEL
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelUserAccess.MODEL_USER_ACCESS
+import edu.uci.ics.texera.dao.jooq.generated.tables.ModelVersion.MODEL_VERSION
 import edu.uci.ics.texera.dao.jooq.generated.tables.User.USER
-import edu.uci.ics.texera.dao.jooq.generated.tables.Dataset.DATASET
-import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetUserAccess.DATASET_USER_ACCESS
-import edu.uci.ics.texera.dao.jooq.generated.tables.DatasetVersion.DATASET_VERSION
-import edu.uci.ics.texera.dao.jooq.generated.tables.daos.{
-  DatasetDao,
-  DatasetUserAccessDao,
-  DatasetVersionDao
-}
-import edu.uci.ics.texera.dao.jooq.generated.tables.pojos.{
-  Dataset,
-  DatasetUserAccess,
-  DatasetVersion
-}
+import edu.uci.ics.texera.dao.jooq.generated.tables.daos.{ModelDao, ModelUserAccessDao, ModelVersionDao}
+import edu.uci.ics.texera.dao.jooq.generated.tables.pojos.{Model, ModelUserAccess, ModelVersion}
 import edu.uci.ics.texera.service.`type`.DatasetFileNode
-import edu.uci.ics.texera.service.resource.DatasetAccessResource.{
-  getDatasetUserAccessPrivilege,
-  getOwner,
-  isDatasetPublic,
-  userHasReadAccess,
-  userHasWriteAccess,
-  userOwnDataset
-}
-import edu.uci.ics.texera.service.resource.DatasetResource.{
-  CreateDatasetRequest,
-  DashboardDataset,
-  DashboardDatasetVersion,
-  DatasetDescriptionModification,
-  DatasetVersionRootFileNodesResponse,
-  Diff,
-  context,
-  getDatasetByID,
-  getDatasetVersionByID,
-  getLatestDatasetVersion,
-  put
-}
+import edu.uci.ics.texera.service.resource.ModelAccessResource._
+import edu.uci.ics.texera.service.resource.ModelResource.{context, _}
 import edu.uci.ics.texera.service.util.S3StorageClient
-import edu.uci.ics.texera.service.util.S3StorageClient.{
-  MAXIMUM_NUM_OF_MULTIPART_S3_PARTS,
-  MINIMUM_NUM_OF_MULTIPART_S3_PART
-}
+import edu.uci.ics.texera.service.util.S3StorageClient.{MAXIMUM_NUM_OF_MULTIPART_S3_PARTS, MINIMUM_NUM_OF_MULTIPART_S3_PART}
 import io.dropwizard.auth.Auth
 import jakarta.annotation.security.RolesAllowed
 import jakarta.ws.rs._
-import jakarta.ws.rs.core.{Context, HttpHeaders, MediaType, Response, StreamingOutput}
+import jakarta.ws.rs.core._
 import org.jooq.{DSLContext, EnumType}
 
-import java.util
 import java.io.{InputStream, OutputStream}
 import java.net.{HttpURLConnection, URL, URLDecoder}
 import java.nio.charset.StandardCharsets
 import java.nio.file.{Files, Paths}
+import java.util
 import java.util.Optional
 import java.util.zip.{ZipEntry, ZipOutputStream}
 import scala.collection.mutable.ListBuffer
 import scala.jdk.CollectionConverters._
 import scala.jdk.OptionConverters._
 
-object DatasetResource {
+object ModelResource {
 
   private val context = SqlServer
     .getInstance()
     .createDSLContext()
 
   /**
-    * Helper function to get the dataset from DB using did
+    * Helper function to get the model from DB using mid
     */
-  private def getDatasetByID(ctx: DSLContext, did: Integer): Dataset = {
-    val datasetDao = new DatasetDao(ctx.configuration())
-    val dataset = datasetDao.fetchOneByDid(did)
-    if (dataset == null) {
-      throw new NotFoundException(f"Dataset $did not found")
+  private def getModelByID(ctx: DSLContext, mid: Integer): Model = {
+    val modelDao = new ModelDao(ctx.configuration())
+    val model = modelDao.fetchOneByMid(mid)
+    if (model == null) {
+      throw new NotFoundException(f"Model $mid not found")
     }
-    dataset
+    model
   }
 
   /**
@@ -125,54 +95,54 @@ object DatasetResource {
   }
 
   /**
-    * Helper function to get the dataset version from DB using dvid
+    * Helper function to get the model version from DB using dvid
     */
-  private def getDatasetVersionByID(
+  private def getModelVersionByID(
       ctx: DSLContext,
-      dvid: Integer
-  ): DatasetVersion = {
-    val datasetVersionDao = new DatasetVersionDao(ctx.configuration())
-    val version = datasetVersionDao.fetchOneByDvid(dvid)
+      mvid: Integer
+  ): ModelVersion = {
+    val modelVersionDao = new ModelVersionDao(ctx.configuration())
+    val version = modelVersionDao.fetchOneByMvid(mvid)
     if (version == null) {
-      throw new NotFoundException("Dataset Version not found")
+      throw new NotFoundException("Model Version not found")
     }
     version
   }
 
   /**
-    * Helper function to get the latest dataset version from the DB
+    * Helper function to get the latest model version from the DB
     */
-  private def getLatestDatasetVersion(
+  private def getLatestModelVersion(
       ctx: DSLContext,
-      did: Integer
-  ): Option[DatasetVersion] = {
+      mid: Integer
+  ): Option[ModelVersion] = {
     ctx
-      .selectFrom(DATASET_VERSION)
-      .where(DATASET_VERSION.DID.eq(did))
-      .orderBy(DATASET_VERSION.CREATION_TIME.desc())
+      .selectFrom(MODEL_VERSION)
+      .where(MODEL_VERSION.MID.eq(mid))
+      .orderBy(MODEL_VERSION.CREATION_TIME.desc())
       .limit(1)
-      .fetchOptionalInto(classOf[DatasetVersion])
+      .fetchOptionalInto(classOf[ModelVersion])
       .toScala
   }
 
-  case class DashboardDataset(
-      dataset: Dataset,
+  case class DashboardModel(
+      model: Model,
       ownerEmail: String,
       accessPrivilege: EnumType,
       isOwner: Boolean,
       size: Long
   )
 
-  case class DashboardDatasetVersion(
-      datasetVersion: DatasetVersion,
-      fileNodes: List[DatasetFileNode]
+  case class DashboardModelVersion(
+                                      modelVersion: ModelVersion,
+                                      fileNodes: List[DatasetFileNode]
   )
 
-  case class CreateDatasetRequest(
-      datasetName: String,
-      datasetDescription: String,
-      isDatasetPublic: Boolean,
-      isDatasetDownloadable: Boolean
+  case class CreateModelRequest(
+                                 modelName: String,
+                                 modelDescription: String,
+                                 isModelPublic: Boolean,
+                                 isModelDownloadable: Boolean
   )
 
   case class Diff(
@@ -182,49 +152,49 @@ object DatasetResource {
       sizeBytes: Option[Long] // Size of the changed file (None for directories)
   )
 
-  case class DatasetDescriptionModification(did: Integer, description: String)
+  case class ModelDescriptionModification(mid: Integer, description: String)
 
-  case class DatasetVersionRootFileNodesResponse(
-      fileNodes: List[DatasetFileNode],
-      size: Long
+  case class ModelVersionRootFileNodesResponse(
+                                                fileNodes: List[DatasetFileNode],
+                                                size: Long
   )
 }
 
 @Produces(Array(MediaType.APPLICATION_JSON, "image/jpeg", "application/pdf"))
-@Path("/dataset")
-class DatasetResource {
-  private val ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE = "User has no access to this dataset"
-  private val ERR_DATASET_VERSION_NOT_FOUND_MESSAGE = "The version of the dataset not found"
+@Path("/model")
+class ModelResource {
+  private val ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE = "User has no access to this model"
+  private val ERR_MODEL_VERSION_NOT_FOUND_MESSAGE = "The version of the model not found"
   private val EXPIRATION_MINUTES = 5
 
   /**
-    * Helper function to get the dataset from DB with additional information including user access privilege and owner email
+    * Helper function to get the model from DB with additional information including user access privilege and owner email
     */
-  private def getDashboardDataset(
+  private def getDashboardModel(
       ctx: DSLContext,
-      did: Integer,
+      mid: Integer,
       requesterUid: Option[Integer]
-  ): DashboardDataset = {
-    val targetDataset = getDatasetByID(ctx, did)
+  ): DashboardModel = {
+    val targetModel = getModelByID(ctx, mid)
 
-    if (requesterUid.isEmpty && !targetDataset.getIsPublic) {
-      throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
-    } else if (requesterUid.exists(uid => !userHasReadAccess(ctx, did, uid))) {
-      throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+    if (requesterUid.isEmpty && !targetModel.getIsPublic) {
+      throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
+    } else if (requesterUid.exists(uid => !userHasReadAccess(ctx, mid, uid))) {
+      throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
     }
 
     val userAccessPrivilege = requesterUid
-      .map(uid => getDatasetUserAccessPrivilege(ctx, did, uid))
+      .map(uid => getModelUserAccessPrivilege(ctx, mid, uid))
       .getOrElse(PrivilegeEnum.READ)
 
-    val isOwner = requesterUid.contains(targetDataset.getOwnerUid)
+    val isOwner = requesterUid.contains(targetModel.getOwnerUid)
 
-    DashboardDataset(
-      targetDataset,
-      getOwner(ctx, did).getEmail,
+    DashboardModel(
+      targetModel,
+      getOwner(ctx, mid).getEmail,
       userAccessPrivilege,
       isOwner,
-      LakeFSStorageClient.retrieveRepositorySize(targetDataset.getName)
+      LakeFSStorageClient.retrieveRepositorySize(targetModel.getName)
     )
   }
 
@@ -233,65 +203,65 @@ class DatasetResource {
   @Path("/create")
   @Consumes(Array(MediaType.APPLICATION_JSON))
   def createDataset(
-      request: CreateDatasetRequest,
-      @Auth user: SessionUser
-  ): DashboardDataset = {
+                     request: CreateModelRequest,
+                     @Auth user: SessionUser
+  ): DashboardModel = {
 
     withTransaction(context) { ctx =>
       val uid = user.getUid
-      val datasetDao: DatasetDao = new DatasetDao(ctx.configuration())
-      val datasetUserAccessDao: DatasetUserAccessDao = new DatasetUserAccessDao(ctx.configuration())
+      val modelDao: ModelDao = new ModelDao(ctx.configuration())
+      val modelUserAccessDao: ModelUserAccessDao = new ModelUserAccessDao(ctx.configuration())
 
-      val datasetName = request.datasetName
-      val datasetDescription = request.datasetDescription
-      val isDatasetPublic = request.isDatasetPublic
-      val isDatasetDownloadable = request.isDatasetDownloadable
+      val modelName = request.modelName
+      val modelDescription = request.modelDescription
+      val isModelPublic = request.isModelPublic
+      val isModelDownloadable = request.isModelDownloadable
 
-      // Check if a dataset with the same name already exists
-      if (!datasetDao.fetchByName(datasetName).isEmpty) {
-        throw new BadRequestException("Dataset with the same name already exists")
+      // Check if a model with the same name already exists
+      if (!modelDao.fetchByName(modelName).isEmpty) {
+        throw new BadRequestException("model with the same name already exists")
       }
 
       // Initialize the repository in LakeFS
       try {
-        LakeFSStorageClient.initRepo(datasetName)
+        LakeFSStorageClient.initRepo(modelName)
       } catch {
         case e: Exception =>
           throw new WebApplicationException(
-            s"Failed to create the dataset: ${e.getMessage}"
+            s"Failed to create the model: ${e.getMessage}"
           )
       }
 
-      // Insert the dataset into the database
-      val dataset = new Dataset()
-      dataset.setName(datasetName)
-      dataset.setDescription(datasetDescription)
-      dataset.setIsPublic(isDatasetPublic)
-      dataset.setIsDownloadable(isDatasetDownloadable)
-      dataset.setOwnerUid(uid)
-
-      val createdDataset = ctx
-        .insertInto(DATASET)
-        .set(ctx.newRecord(DATASET, dataset))
+      // Insert the model into the database
+      val model = new Model()
+      model.setName(modelName)
+      model.setDescription(modelDescription)
+      model.setIsPublic(isModelPublic)
+      model.setIsDownloadable(isModelDownloadable)
+      model.setOwnerUid(uid)
+
+      val createdModel = ctx
+        .insertInto(MODEL)
+        .set(ctx.newRecord(MODEL, model))
         .returning()
         .fetchOne()
 
-      // Insert the requester as the WRITE access user for this dataset
-      val datasetUserAccess = new DatasetUserAccess()
-      datasetUserAccess.setDid(createdDataset.getDid)
-      datasetUserAccess.setUid(uid)
-      datasetUserAccess.setPrivilege(PrivilegeEnum.WRITE)
-      datasetUserAccessDao.insert(datasetUserAccess)
-
-      DashboardDataset(
-        new Dataset(
-          createdDataset.getDid,
-          createdDataset.getOwnerUid,
-          createdDataset.getName,
-          createdDataset.getIsPublic,
-          createdDataset.getDescription,
-          createdDataset.getCreationTime,
-          createdDataset.getIsDownloadable
+      // Insert the requester as the WRITE access user for this model
+      val modelUserAccess = new ModelUserAccess()
+      modelUserAccess.setMid(createdModel.getMid)
+      modelUserAccess.setUid(uid)
+      modelUserAccess.setPrivilege(PrivilegeEnum.WRITE)
+      modelUserAccessDao.insert(modelUserAccess)
+
+      DashboardModel(
+        new Model(
+          createdModel.getMid,
+          createdModel.getOwnerUid,
+          createdModel.getName,
+          createdModel.getIsPublic,
+          createdModel.getIsDownloadable,
+          createdModel.getDescription,
+          createdModel.getCreationTime,
         ),
         user.getEmail,
         PrivilegeEnum.WRITE,
@@ -303,28 +273,28 @@ class DatasetResource {
 
   @POST
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/version/create")
+  @Path("/{mid}/version/create")
   @Consumes(Array(MediaType.TEXT_PLAIN))
   def createDatasetVersion(
       versionName: String,
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @Auth user: SessionUser
-  ): DashboardDatasetVersion = {
+  ): DashboardModelVersion = {
     val uid = user.getUid
     withTransaction(context) { ctx =>
-      if (!userHasWriteAccess(ctx, did, uid)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      if (!userHasWriteAccess(ctx, mid, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
 
-      val dataset = getDatasetByID(ctx, did)
-      val datasetName = dataset.getName
+      val model = getModelByID(ctx, mid)
+      val modelName = model.getName
 
       // Check if there are any changes in LakeFS before creating a new version
-      val diffs = LakeFSStorageClient.retrieveUncommittedObjects(repoName = datasetName)
+      val diffs = LakeFSStorageClient.retrieveUncommittedObjects(repoName = modelName)
 
       if (diffs.isEmpty) {
         throw new WebApplicationException(
-          "No changes detected in dataset. Version creation aborted.",
+          "No changes detected in model. Version creation aborted.",
           Response.Status.BAD_REQUEST
         )
       }
@@ -332,8 +302,8 @@ class DatasetResource {
       // Generate a new version name
       val versionCount = ctx
         .selectCount()
-        .from(DATASET_VERSION)
-        .where(DATASET_VERSION.DID.eq(did))
+        .from(MODEL_VERSION)
+        .where(MODEL_VERSION.MID.eq(mid))
         .fetchOne(0, classOf[Int])
 
       val sanitizedVersionName = Option(versionName).filter(_.nonEmpty).getOrElse("")
@@ -345,9 +315,9 @@ class DatasetResource {
 
       // Create a commit in LakeFS
       val commit = LakeFSStorageClient.createCommit(
-        repoName = datasetName,
+        repoName = modelName,
         branch = "main",
-        commitMessage = s"Created dataset version: $newVersionName"
+        commitMessage = s"Created model version: $newVersionName"
       )
 
       if (commit == null || commit.getId == null) {
@@ -357,28 +327,28 @@ class DatasetResource {
         )
       }
 
-      // Create a new dataset version entry in the database
-      val datasetVersion = new DatasetVersion()
-      datasetVersion.setDid(did)
-      datasetVersion.setCreatorUid(uid)
-      datasetVersion.setName(newVersionName)
-      datasetVersion.setVersionHash(commit.getId) // Store LakeFS version hash
+      // Create a new model version entry in the database
+      val modelVersion = new ModelVersion()
+      modelVersion.setMid(mid)
+      modelVersion.setCreatorUid(uid)
+      modelVersion.setName(newVersionName)
+      modelVersion.setVersionHash(commit.getId) // Store LakeFS version hash
 
       val insertedVersion = ctx
-        .insertInto(DATASET_VERSION)
-        .set(ctx.newRecord(DATASET_VERSION, datasetVersion))
+        .insertInto(MODEL_VERSION)
+        .set(ctx.newRecord(MODEL_VERSION, modelVersion))
         .returning()
         .fetchOne()
-        .into(classOf[DatasetVersion])
+        .into(classOf[ModelVersion])
 
       // Retrieve committed file structure
-      val fileNodes = LakeFSStorageClient.retrieveObjectsOfVersion(datasetName, commit.getId)
+      val fileNodes = LakeFSStorageClient.retrieveObjectsOfVersion(modelName, commit.getId)
 
-      DashboardDatasetVersion(
+      DashboardModelVersion(
         insertedVersion,
         DatasetFileNode
           .fromLakeFSRepositoryCommittedObjects(
-            Map((user.getEmail, datasetName, newVersionName) -> fileNodes)
+            Map((user.getEmail, modelName, newVersionName) -> fileNodes)
           )
       )
     }
@@ -386,18 +356,18 @@ class DatasetResource {
 
   @DELETE
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}")
-  def deleteDataset(@PathParam("did") did: Integer, @Auth user: SessionUser): Response = {
+  @Path("/{mid}")
+  def deleteDataset(@PathParam("mid") mid: Integer, @Auth user: SessionUser): Response = {
     val uid = user.getUid
     withTransaction(context) { ctx =>
-      val datasetDao = new DatasetDao(ctx.configuration())
-      val dataset = getDatasetByID(ctx, did)
-      if (!userOwnDataset(ctx, dataset.getDid, uid)) {
-        // throw the exception that user has no access to certain dataset
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      val modelDao = new ModelDao(ctx.configuration())
+      val model = getModelByID(ctx, mid)
+      if (!userOwnDataset(ctx, model.getMid, uid)) {
+        // throw the exception that user has no access to certain model
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
       try {
-        LakeFSStorageClient.deleteRepo(dataset.getName)
+        LakeFSStorageClient.deleteRepo(model.getName)
       } catch {
         case e: Exception =>
           throw new WebApplicationException(
@@ -407,12 +377,12 @@ class DatasetResource {
       }
 
       // delete the directory on S3
-      if (S3StorageClient.directoryExists(StorageConfig.lakefsBucketName, dataset.getName)) {
-        S3StorageClient.deleteDirectory(StorageConfig.lakefsBucketName, dataset.getName)
+      if (S3StorageClient.directoryExists(StorageConfig.lakefsBucketName, model.getName)) {
+        S3StorageClient.deleteDirectory(StorageConfig.lakefsBucketName, model.getName)
       }
 
-      // delete the dataset from the DB
-      datasetDao.deleteById(dataset.getDid)
+      // delete the model from the DB
+      modelDao.deleteById(model.getMid)
 
       Response.ok().build()
     }
@@ -423,30 +393,30 @@ class DatasetResource {
   @Produces(Array(MediaType.APPLICATION_JSON))
   @RolesAllowed(Array("REGULAR", "ADMIN"))
   @Path("/update/description")
-  def updateDatasetDescription(
-      modificator: DatasetDescriptionModification,
-      @Auth sessionUser: SessionUser
+  def updateModelDescription(
+                                modificator: ModelDescriptionModification,
+                                @Auth sessionUser: SessionUser
   ): Response = {
     withTransaction(context) { ctx =>
       val uid = sessionUser.getUid
-      val datasetDao = new DatasetDao(ctx.configuration())
-      val dataset = getDatasetByID(ctx, modificator.did)
-      if (!userHasWriteAccess(ctx, modificator.did, uid)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      val modelDao = new ModelDao(ctx.configuration())
+      val model = getModelByID(ctx, modificator.mid)
+      if (!userHasWriteAccess(ctx, modificator.mid, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
 
-      dataset.setDescription(modificator.description)
-      datasetDao.update(dataset)
+      model.setDescription(modificator.description)
+      modelDao.update(model)
       Response.ok().build()
     }
   }
 
   @POST
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/upload")
+  @Path("/{mid}/upload")
   @Consumes(Array(MediaType.APPLICATION_OCTET_STREAM))
-  def uploadOneFileToDataset(
-      @PathParam("did") did: Integer,
+  def uploadOneFileToModel(
+      @PathParam("mid") mid: Integer,
       @QueryParam("filePath") encodedFilePath: String,
       @QueryParam("message") message: String,
       fileStream: InputStream,
@@ -462,11 +432,11 @@ class DatasetResource {
 
     try {
       withTransaction(context) { ctx =>
-        if (!userHasWriteAccess(ctx, did, uid))
-          throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+        if (!userHasWriteAccess(ctx, mid, uid))
+          throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
 
-        val dataset = getDatasetByID(ctx, did)
-        repoName = dataset.getName
+        val model = getModelByID(ctx, mid)
+        repoName = model.getName
         filePath = URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name)
 
         // ---------- decide part-size & number-of-parts ----------
@@ -549,7 +519,7 @@ class DatasetResource {
           )
         }
         throw new WebApplicationException(
-          s"Failed to upload file to dataset: ${e.getMessage}",
+          s"Failed to upload file to model: ${e.getMessage}",
           e
         )
     }
@@ -559,69 +529,69 @@ class DatasetResource {
   @RolesAllowed(Array("REGULAR", "ADMIN"))
   @Path("/presign-download")
   def getPresignedUrl(
-      @QueryParam("filePath") encodedUrl: String,
-      @QueryParam("datasetName") datasetName: String,
-      @QueryParam("commitHash") commitHash: String,
-      @Auth user: SessionUser
+                       @QueryParam("filePath") encodedUrl: String,
+                       @QueryParam("modelName") modelName: String,
+                       @QueryParam("commitHash") commitHash: String,
+                       @Auth user: SessionUser
   ): Response = {
     val uid = user.getUid
-    generatePresignedResponse(encodedUrl, datasetName, commitHash, uid)
+    generatePresignedResponse(encodedUrl, modelName, commitHash, uid)
   }
 
   @GET
   @RolesAllowed(Array("REGULAR", "ADMIN"))
   @Path("/presign-download-s3")
   def getPresignedUrlWithS3(
-      @QueryParam("filePath") encodedUrl: String,
-      @QueryParam("datasetName") datasetName: String,
-      @QueryParam("commitHash") commitHash: String,
-      @Auth user: SessionUser
+                             @QueryParam("filePath") encodedUrl: String,
+                             @QueryParam("modelName") modelName: String,
+                             @QueryParam("commitHash") commitHash: String,
+                             @Auth user: SessionUser
   ): Response = {
     val uid = user.getUid
-    generatePresignedResponse(encodedUrl, datasetName, commitHash, uid)
+    generatePresignedResponse(encodedUrl, modelName, commitHash, uid)
   }
 
   @GET
   @Path("/public-presign-download")
   def getPublicPresignedUrl(
       @QueryParam("filePath") encodedUrl: String,
-      @QueryParam("datasetName") datasetName: String,
+      @QueryParam("modelName") modelName: String,
       @QueryParam("commitHash") commitHash: String
   ): Response = {
-    generatePresignedResponse(encodedUrl, datasetName, commitHash, null)
+    generatePresignedResponse(encodedUrl, modelName, commitHash, null)
   }
 
   @GET
   @Path("/public-presign-download-s3")
   def getPublicPresignedUrlWithS3(
       @QueryParam("filePath") encodedUrl: String,
-      @QueryParam("datasetName") datasetName: String,
+      @QueryParam("modelName") modelName: String,
       @QueryParam("commitHash") commitHash: String
   ): Response = {
-    generatePresignedResponse(encodedUrl, datasetName, commitHash, null)
+    generatePresignedResponse(encodedUrl, modelName, commitHash, null)
   }
 
   @DELETE
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/file")
+  @Path("/{mid}/file")
   @Consumes(Array(MediaType.APPLICATION_JSON))
   def deleteDatasetFile(
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @QueryParam("filePath") encodedFilePath: String,
       @Auth user: SessionUser
   ): Response = {
     val uid = user.getUid
     withTransaction(context) { ctx =>
-      if (!userHasWriteAccess(ctx, did, uid)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      if (!userHasWriteAccess(ctx, mid, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
-      val datasetName = getDatasetByID(ctx, did).getName
+      val modelName = getModelByID(ctx, mid).getName
 
       // Decode the file path
       val filePath = URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name())
       // Try to initialize the repository in LakeFS
       try {
-        LakeFSStorageClient.deleteObject(datasetName, filePath)
+        LakeFSStorageClient.deleteObject(modelName, filePath)
       } catch {
         case e: Exception =>
           throw new WebApplicationException(
@@ -638,24 +608,24 @@ class DatasetResource {
   @Path("/multipart-upload")
   @Consumes(Array(MediaType.APPLICATION_JSON))
   def multipartUpload(
-      @QueryParam("datasetName") datasetName: String,
-      @QueryParam("type") operationType: String,
-      @QueryParam("filePath") encodedUrl: String,
-      @QueryParam("uploadId") uploadId: Optional[String],
-      @QueryParam("numParts") numParts: Optional[Integer],
-      payload: Map[
+                       @QueryParam("modelName") modelName: String,
+                       @QueryParam("type") operationType: String,
+                       @QueryParam("filePath") encodedUrl: String,
+                       @QueryParam("uploadId") uploadId: Optional[String],
+                       @QueryParam("numParts") numParts: Optional[Integer],
+                       payload: Map[
         String,
         Any
       ], // Expecting {"parts": [...], "physicalAddress": "s3://bucket/path"}
-      @Auth user: SessionUser
+                       @Auth user: SessionUser
   ): Response = {
     val uid = user.getUid
 
     withTransaction(context) { ctx =>
-      val datasetDao = new DatasetDao(ctx.configuration())
-      val datasets = datasetDao.fetchByName(datasetName).asScala.toList
-      if (datasets.isEmpty || !userHasWriteAccess(ctx, datasets.head.getDid, uid)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      val modelDao = new ModelDao(ctx.configuration())
+      val models = modelDao.fetchByName(modelName).asScala.toList
+      if (models.isEmpty || !userHasWriteAccess(ctx, models.head.getMid, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
 
       // Decode the file path
@@ -668,7 +638,7 @@ class DatasetResource {
           )
 
           val presignedResponse = LakeFSStorageClient.initiatePresignedMultipartUploads(
-            datasetName,
+            modelName,
             filePath,
             numPartsValue
           )
@@ -725,7 +695,7 @@ class DatasetResource {
 
           // Complete the multipart upload with parts and physical address
           val objectStats = LakeFSStorageClient.completePresignedMultipartUploads(
-            datasetName,
+            modelName,
             filePath,
             uploadIdValue,
             partsList,
@@ -742,8 +712,8 @@ class DatasetResource {
             .build()
 
         case "abort" =>
-          val uploadIdValue = uploadId.toScala.getOrElse(
-            throw new BadRequestException("uploadId is required for abortion")
+          val uploamidValue = uploadId.toScala.getOrElse(
+            throw new BadRequestException("uploamid is required for abortion")
           )
 
           // Extract physical address from payload
@@ -754,9 +724,9 @@ class DatasetResource {
 
           // Abort the multipart upload
           LakeFSStorageClient.abortPresignedMultipartUploads(
-            datasetName,
+            modelName,
             filePath,
-            uploadIdValue,
+            uploamidValue,
             physicalAddress
           )
 
@@ -770,73 +740,73 @@ class DatasetResource {
 
   @POST
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/update/publicity")
+  @Path("/{mid}/update/publicity")
   def toggleDatasetPublicity(
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @Auth sessionUser: SessionUser
   ): Response = {
     withTransaction(context) { ctx =>
-      val datasetDao = new DatasetDao(ctx.configuration())
+      val modelDao = new ModelDao(ctx.configuration())
       val uid = sessionUser.getUid
 
-      if (!userHasWriteAccess(ctx, did, uid)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      if (!userHasWriteAccess(ctx, mid, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
 
-      val existedDataset = getDatasetByID(ctx, did)
-      val newPublicStatus = !existedDataset.getIsPublic
-      existedDataset.setIsPublic(newPublicStatus)
+      val existedModel = getModelByID(ctx, mid)
+      val newPublicStatus = !existedModel.getIsPublic
+      existedModel.setIsPublic(newPublicStatus)
 
-      datasetDao.update(existedDataset)
+      modelDao.update(existedModel)
       Response.ok().build()
     }
   }
 
   @POST
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/update/downloadable")
+  @Path("/{mid}/update/downloadable")
   def toggleDatasetDownloadable(
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @Auth sessionUser: SessionUser
   ): Response = {
     withTransaction(context) { ctx =>
-      val datasetDao = new DatasetDao(ctx.configuration())
+      val modelDao = new ModelDao(ctx.configuration())
       val uid = sessionUser.getUid
 
-      if (!userOwnDataset(ctx, did, uid)) {
-        throw new ForbiddenException("Only dataset owners can modify download permissions")
+      if (!userOwnDataset(ctx, mid, uid)) {
+        throw new ForbiddenException("Only model owners can modify download permissions")
       }
 
-      val existedDataset = getDatasetByID(ctx, did)
-      val newDownloadableStatus = !existedDataset.getIsDownloadable
+      val existedModel = getModelByID(ctx, mid)
+      val newDownloadableStatus = !existedModel.getIsDownloadable
 
-      existedDataset.setIsDownloadable(newDownloadableStatus)
+      existedModel.setIsDownloadable(newDownloadableStatus)
 
-      datasetDao.update(existedDataset)
+      modelDao.update(existedModel)
       Response.ok().build()
     }
   }
 
   @GET
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/diff")
+  @Path("/{mid}/diff")
   def getDatasetDiff(
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @Auth user: SessionUser
   ): List[Diff] = {
     val uid = user.getUid
     withTransaction(context) { ctx =>
-      if (!userHasReadAccess(ctx, did, uid)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      if (!userHasReadAccess(ctx, mid, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
 
       // Retrieve staged (uncommitted) changes from LakeFS
-      val dataset = getDatasetByID(ctx, did)
-      val lakefsDiffs = LakeFSStorageClient.retrieveUncommittedObjects(dataset.getName)
+      val model = getModelByID(ctx, mid)
+      val lakefsDiffs = LakeFSStorageClient.retrieveUncommittedObjects(model.getName)
 
       // Convert LakeFS Diff objects to our custom Diff case class
       lakefsDiffs.map(d =>
-        new Diff(
+        Diff(
           d.getPath,
           d.getPathType.getValue,
           d.getType.getValue,
@@ -848,25 +818,25 @@ class DatasetResource {
 
   @PUT
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/diff")
+  @Path("/{mid}/diff")
   @Consumes(Array(MediaType.APPLICATION_JSON))
   def resetDatasetFileDiff(
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @QueryParam("filePath") encodedFilePath: String,
       @Auth user: SessionUser
   ): Response = {
     val uid = user.getUid
     withTransaction(context) { ctx =>
-      if (!userHasWriteAccess(ctx, did, uid)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      if (!userHasWriteAccess(ctx, mid, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
-      val datasetName = getDatasetByID(ctx, did).getName
+      val modelName = getModelByID(ctx, mid).getName
 
       // Decode the file path
       val filePath = URLDecoder.decode(encodedFilePath, StandardCharsets.UTF_8.name())
       // Try to reset the file change in LakeFS
       try {
-        LakeFSStorageClient.resetObjectUploadOrDeletion(datasetName, filePath)
+        LakeFSStorageClient.resetObjectUploadOrDeletion(modelName, filePath)
       } catch {
         case e: Exception =>
           throw new WebApplicationException(
@@ -878,40 +848,40 @@ class DatasetResource {
   }
 
   /**
-    * This method returns a list of DashboardDatasets objects that are accessible by current user.
+    * This method returns a list of Dashboardmodels objects that are accessible by current user.
     *
     * @param user the session user
-    * @return list of user accessible DashboardDataset objects
+    * @return list of user accessible Dashboardmodel objects
     */
   @GET
   @RolesAllowed(Array("REGULAR", "ADMIN"))
   @Path("/list")
   def listDatasets(
       @Auth user: SessionUser
-  ): List[DashboardDataset] = {
+  ): List[DashboardModel] = {
     val uid = user.getUid
     withTransaction(context)(ctx => {
-      var accessibleDatasets: ListBuffer[DashboardDataset] = ListBuffer()
-      // first fetch all datasets user have explicit access to
-      accessibleDatasets = ListBuffer.from(
+      var accessibleModels: ListBuffer[DashboardModel] = ListBuffer()
+      // first fetch all models user have explicit access to
+      accessibleModels = ListBuffer.from(
         ctx
           .select()
           .from(
-            DATASET
-              .leftJoin(DATASET_USER_ACCESS)
-              .on(DATASET_USER_ACCESS.DID.eq(DATASET.DID))
+            MODEL
+              .leftJoin(MODEL_USER_ACCESS)
+              .on(MODEL_USER_ACCESS.MID.eq(MODEL.MID))
               .leftJoin(USER)
-              .on(USER.UID.eq(DATASET.OWNER_UID))
+              .on(USER.UID.eq(MODEL.OWNER_UID))
           )
-          .where(DATASET_USER_ACCESS.UID.eq(uid))
+          .where(MODEL_USER_ACCESS.UID.eq(uid))
           .fetch()
           .map(record => {
-            val dataset = record.into(DATASET).into(classOf[Dataset])
-            val datasetAccess = record.into(DATASET_USER_ACCESS).into(classOf[DatasetUserAccess])
+            val model = record.into(MODEL).into(classOf[Model])
+            val datasetAccess = record.into(MODEL_USER_ACCESS).into(classOf[ModelUserAccess])
             val ownerEmail = record.into(USER).getEmail
-            DashboardDataset(
-              isOwner = dataset.getOwnerUid == uid,
-              dataset = dataset,
+            DashboardModel(
+              isOwner = model.getOwnerUid == uid,
+              model = model,
               accessPrivilege = datasetAccess.getPrivilege,
               ownerEmail = ownerEmail,
               size = 0
@@ -920,88 +890,88 @@ class DatasetResource {
           .asScala
       )
 
-      // then we fetch the public datasets and merge it as a part of the result if not exist
-      val publicDatasets = ctx
+      // then we fetch the public models and merge it as a part of the result if not exist
+      val publicModels = ctx
         .select()
         .from(
-          DATASET
+          MODEL
             .leftJoin(USER)
-            .on(USER.UID.eq(DATASET.OWNER_UID))
+            .on(USER.UID.eq(MODEL.OWNER_UID))
         )
-        .where(DATASET.IS_PUBLIC.eq(true))
+        .where(MODEL.IS_PUBLIC.eq(true))
         .fetch()
         .map(record => {
-          val dataset = record.into(DATASET).into(classOf[Dataset])
+          val model = record.into(MODEL).into(classOf[Model])
           val ownerEmail = record.into(USER).getEmail
-          DashboardDataset(
+          DashboardModel(
             isOwner = false,
-            dataset = dataset,
+            model = model,
             accessPrivilege = PrivilegeEnum.READ,
             ownerEmail = ownerEmail,
-            size = LakeFSStorageClient.retrieveRepositorySize(dataset.getName)
+            size = LakeFSStorageClient.retrieveRepositorySize(model.getName)
           )
         })
-      publicDatasets.forEach { publicDataset =>
-        if (!accessibleDatasets.exists(_.dataset.getDid == publicDataset.dataset.getDid)) {
-          val dashboardDataset = DashboardDataset(
+      publicModels.forEach { publicModel =>
+        if (!accessibleModels.exists(_.model.getMid == publicModel.model.getMid)) {
+          val dashboardDataset = DashboardModel(
             isOwner = false,
-            dataset = publicDataset.dataset,
-            ownerEmail = publicDataset.ownerEmail,
+            model = publicModel.model,
+            ownerEmail = publicModel.ownerEmail,
             accessPrivilege = PrivilegeEnum.READ,
-            size = LakeFSStorageClient.retrieveRepositorySize(publicDataset.dataset.getName)
+            size = LakeFSStorageClient.retrieveRepositorySize(publicModel.model.getName)
           )
-          accessibleDatasets = accessibleDatasets :+ dashboardDataset
+          accessibleModels = accessibleModels :+ dashboardDataset
         }
       }
-      accessibleDatasets.toList
+      accessibleModels.toList
     })
   }
 
   @GET
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/version/list")
+  @Path("/{mid}/version/list")
   def getDatasetVersionList(
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @Auth user: SessionUser
-  ): List[DatasetVersion] = {
+  ): List[ModelVersion] = {
     val uid = user.getUid
     withTransaction(context)(ctx => {
-      val dataset = getDatasetByID(ctx, did)
-      if (!userHasReadAccess(ctx, dataset.getDid, uid)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      val model = getModelByID(ctx, mid)
+      if (!userHasReadAccess(ctx, model.getMid, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
-      fetchDatasetVersions(ctx, dataset.getDid)
+      fetchModelVersions(ctx, model.getMid)
     })
   }
 
   @GET
   @Path("/{name}/publicVersion/list")
   def getPublicDatasetVersionList(
-      @PathParam("name") did: Integer
-  ): List[DatasetVersion] = {
+      @PathParam("name") mid: Integer
+  ): List[ModelVersion] = {
     withTransaction(context)(ctx => {
-      if (!isDatasetPublic(ctx, did)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      if (!isDatasetPublic(ctx, mid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
-      fetchDatasetVersions(ctx, did)
+      fetchModelVersions(ctx, mid)
     })
   }
 
   @GET
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/version/latest")
+  @Path("/{mid}/version/latest")
   def retrieveLatestDatasetVersion(
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @Auth user: SessionUser
-  ): DashboardDatasetVersion = {
+  ): DashboardModelVersion = {
     val uid = user.getUid
     withTransaction(context)(ctx => {
-      if (!userHasReadAccess(ctx, did, uid)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      if (!userHasReadAccess(ctx, mid, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
-      val dataset = getDatasetByID(ctx, did)
-      val latestVersion = getLatestDatasetVersion(ctx, did).getOrElse(
-        throw new NotFoundException(ERR_DATASET_VERSION_NOT_FOUND_MESSAGE)
+      val dataset = getModelByID(ctx, mid)
+      val latestVersion = getLatestModelVersion(ctx, mid).getOrElse(
+        throw new NotFoundException(ERR_MODEL_VERSION_NOT_FOUND_MESSAGE)
       )
 
       val ownerNode = DatasetFileNode
@@ -1014,7 +984,7 @@ class DatasetResource {
         )
         .head
 
-      DashboardDatasetVersion(
+      DashboardModelVersion(
         latestVersion,
         ownerNode.children.get
           .find(_.getName == dataset.getName)
@@ -1031,10 +1001,10 @@ class DatasetResource {
 
   @GET
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/versionZip")
+  @Path("/{mid}/versionZip")
   def getDatasetVersionZip(
-      @PathParam("did") did: Integer,
-      @QueryParam("dvid") dvid: Integer, // Dataset version ID, nullable
+      @PathParam("mid") mid: Integer,
+      @QueryParam("dvid") dvid: Integer, // model version ID, nullable
       @QueryParam("latest") latest: java.lang.Boolean, // Flag to get latest version, nullable
       @Auth user: SessionUser
   ): Response = {
@@ -1046,29 +1016,29 @@ class DatasetResource {
 
       // Check read access and download permission
       val uid = user.getUid
-      if (!userHasReadAccess(ctx, did, uid)) {
-        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+      if (!userHasReadAccess(ctx, mid, uid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
       }
 
-      // Retrieve dataset and check download permission
-      val dataset = getDatasetByID(ctx, did)
-      // Non-owners can download if dataset is downloadable and they have read access
-      if (!userOwnDataset(ctx, did, uid) && !dataset.getIsDownloadable) {
-        throw new ForbiddenException("Dataset download is not allowed")
+      // Retrieve model and check download permission
+      val dataset = getModelByID(ctx, mid)
+      // Non-owners can download if model is downloadable and they have read access
+      if (!userOwnDataset(ctx, mid, uid) && !dataset.getIsDownloadable) {
+        throw new ForbiddenException("model download is not allowed")
       }
 
       // Determine which version to retrieve
       val datasetVersion = if (dvid != null) {
-        getDatasetVersionByID(ctx, dvid)
+        getModelVersionByID(ctx, dvid)
       } else if (java.lang.Boolean.TRUE.equals(latest)) {
-        getLatestDatasetVersion(ctx, did).getOrElse(
-          throw new NotFoundException(ERR_DATASET_VERSION_NOT_FOUND_MESSAGE)
+        getLatestModelVersion(ctx, mid).getOrElse(
+          throw new NotFoundException(ERR_MODEL_VERSION_NOT_FOUND_MESSAGE)
         )
       } else {
         throw new BadRequestException("Invalid parameters")
       }
 
-      // Retrieve dataset and version details
+      // Retrieve model and version details
       val datasetName = dataset.getName
       val versionHash = datasetVersion.getVersionHash
       val objects = LakeFSStorageClient.retrieveObjectsOfVersion(datasetName, versionHash)
@@ -1110,42 +1080,42 @@ class DatasetResource {
 
   @GET
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}/version/{dvid}/rootFileNodes")
+  @Path("/{mid}/version/{dvid}/rootFileNodes")
   def retrieveDatasetVersionRootFileNodes(
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @PathParam("dvid") dvid: Integer,
       @Auth user: SessionUser
-  ): DatasetVersionRootFileNodesResponse = {
+  ): ModelVersionRootFileNodesResponse = {
     val uid = user.getUid
-    withTransaction(context)(ctx => fetchDatasetVersionRootFileNodes(ctx, did, dvid, Some(uid)))
+    withTransaction(context)(ctx => fetchModelVersionRootFileNodes(ctx, mid, dvid, Some(uid)))
   }
 
   @GET
-  @Path("/{did}/publicVersion/{dvid}/rootFileNodes")
+  @Path("/{mid}/publicVersion/{dvid}/rootFileNodes")
   def retrievePublicDatasetVersionRootFileNodes(
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @PathParam("dvid") dvid: Integer
-  ): DatasetVersionRootFileNodesResponse = {
-    withTransaction(context)(ctx => fetchDatasetVersionRootFileNodes(ctx, did, dvid, None))
+  ): ModelVersionRootFileNodesResponse = {
+    withTransaction(context)(ctx => fetchModelVersionRootFileNodes(ctx, mid, dvid, None))
   }
 
   @GET
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/{did}")
+  @Path("/{mid}")
   def getDataset(
-      @PathParam("did") did: Integer,
+      @PathParam("mid") mid: Integer,
       @Auth user: SessionUser
-  ): DashboardDataset = {
+  ): DashboardModel = {
     val uid = user.getUid
-    withTransaction(context)(ctx => getDashboardDataset(ctx, did, Some(uid)))
+    withTransaction(context)(ctx => getDashboardModel(ctx, mid, Some(uid)))
   }
 
   @GET
-  @Path("/public/{did}")
+  @Path("/public/{mid}")
   def getPublicDataset(
-      @PathParam("did") did: Integer
-  ): DashboardDataset = {
-    withTransaction(context)(ctx => getDashboardDataset(ctx, did, None))
+      @PathParam("mid") mid: Integer
+  ): DashboardModel = {
+    withTransaction(context)(ctx => getDashboardModel(ctx, mid, None))
   }
 
   @GET
@@ -1195,61 +1165,61 @@ class DatasetResource {
   }
 
   /**
-    * This method returns all owner user names of the dataset that the user has access to
+    * This method returns all owner user names of the model that the user has access to
     *
     * @return OwnerName[]
     */
   @GET
   @RolesAllowed(Array("REGULAR", "ADMIN"))
-  @Path("/user-dataset-owners")
+  @Path("/user-model-owners")
   def retrieveOwners(@Auth user: SessionUser): util.List[String] = {
     context
       .selectDistinct(USER.EMAIL)
       .from(USER)
-      .join(DATASET)
-      .on(DATASET.OWNER_UID.eq(USER.UID))
-      .join(DATASET_USER_ACCESS)
-      .on(DATASET_USER_ACCESS.DID.eq(DATASET.DID))
-      .where(DATASET_USER_ACCESS.UID.eq(user.getUid))
+      .join(MODEL)
+      .on(MODEL.OWNER_UID.eq(USER.UID))
+      .join(MODEL_USER_ACCESS)
+      .on(MODEL_USER_ACCESS.MID.eq(MODEL.MID))
+      .where(MODEL_USER_ACCESS.UID.eq(user.getUid))
       .fetchInto(classOf[String])
   }
 
-  private def fetchDatasetVersions(ctx: DSLContext, did: Integer): List[DatasetVersion] = {
+  private def fetchModelVersions(ctx: DSLContext, mid: Integer): List[ModelVersion] = {
     ctx
-      .selectFrom(DATASET_VERSION)
-      .where(DATASET_VERSION.DID.eq(did))
-      .orderBy(DATASET_VERSION.CREATION_TIME.desc()) // Change to .asc() for ascending order
-      .fetchInto(classOf[DatasetVersion])
+      .selectFrom(MODEL_VERSION)
+      .where(MODEL_VERSION.MID.eq(mid))
+      .orderBy(MODEL_VERSION.CREATION_TIME.desc()) // Change to .asc() for ascending order
+      .fetchInto(classOf[ModelVersion])
       .asScala
       .toList
   }
 
-  private def fetchDatasetVersionRootFileNodes(
+  private def fetchModelVersionRootFileNodes(
       ctx: DSLContext,
-      did: Integer,
+      mid: Integer,
       dvid: Integer,
       uid: Option[Integer]
-  ): DatasetVersionRootFileNodesResponse = {
-    val dataset = getDashboardDataset(ctx, did, uid)
-    val datasetVersion = getDatasetVersionByID(ctx, dvid)
-    val datasetName = dataset.dataset.getName
+  ): ModelVersionRootFileNodesResponse = {
+    val model = getDashboardModel(ctx, mid, uid)
+    val modelVersion = getModelVersionByID(ctx, dvid)
+    val modelName = model.model.getName
 
     val ownerFileNode = DatasetFileNode
       .fromLakeFSRepositoryCommittedObjects(
         Map(
-          (dataset.ownerEmail, datasetName, datasetVersion.getName) -> LakeFSStorageClient
-            .retrieveObjectsOfVersion(datasetName, datasetVersion.getVersionHash)
+          (model.ownerEmail, modelName, modelVersion.getName) -> LakeFSStorageClient
+            .retrieveObjectsOfVersion(modelName, modelVersion.getVersionHash)
         )
       )
       .head
 
-    DatasetVersionRootFileNodesResponse(
+    ModelVersionRootFileNodesResponse(
       ownerFileNode.children.get
-        .find(_.getName == datasetName)
+        .find(_.getName == modelName)
         .head
         .children
         .get
-        .find(_.getName == datasetVersion.getName)
+        .find(_.getName == modelVersion.getName)
         .head
         .children
         .get,
@@ -1258,18 +1228,18 @@ class DatasetResource {
   }
 
   private def generatePresignedResponse(
-      encodedUrl: String,
-      datasetName: String,
-      commitHash: String,
-      uid: Integer
+                                         encodedUrl: String,
+                                         modelName: String,
+                                         commitHash: String,
+                                         uid: Integer
   ): Response = {
-    resolveDatasetAndPath(encodedUrl, datasetName, commitHash, uid) match {
+    resolveModelAndPath(encodedUrl, modelName, commitHash, uid) match {
       case Left(errorResponse) =>
         errorResponse
 
-      case Right((resolvedDatasetName, resolvedCommitHash, resolvedFilePath)) =>
+      case Right((resolvedModelName, resolvedCommitHash, resolvedFilePath)) =>
         val url = LakeFSStorageClient.getFilePresignedUrl(
-          resolvedDatasetName,
+          resolvedModelName,
           resolvedCommitHash,
           resolvedFilePath
         )
@@ -1278,57 +1248,56 @@ class DatasetResource {
     }
   }
 
-  private def resolveDatasetAndPath(
-      encodedUrl: String,
-      datasetName: String,
-      commitHash: String,
-      uid: Integer
+  private def resolveModelAndPath(
+                                   encodedUrl: String,
+                                   modelName: String,
+                                   commitHash: String,
+                                   uid: Integer
   ): Either[Response, (String, String, String)] = {
     val decodedPathStr = URLDecoder.decode(encodedUrl, StandardCharsets.UTF_8.name())
 
-    (Option(datasetName), Option(commitHash)) match {
+    (Option(modelName), Option(commitHash)) match {
       case (Some(_), None) | (None, Some(_)) =>
         // Case 1: Only one parameter is provided (error case)
         Left(
           Response
             .status(Response.Status.BAD_REQUEST)
             .entity(
-              "Both datasetName and commitHash must be provided together, or neither should be provided."
+              "Both modelName and commitHash must be provided together, or neither should be provided."
             )
             .build()
         )
 
       case (Some(dsName), Some(commit)) =>
-        // Case 2: datasetName and commitHash are provided, validate access
+        // Case 2: modelName and commitHash are provided, validate access
         val response = withTransaction(context) { ctx =>
-          val datasetDao = new DatasetDao(ctx.configuration())
-          val datasets = datasetDao.fetchByName(dsName).asScala.toList
+          val modelDao = new ModelDao(ctx.configuration())
+          val models = modelDao.fetchByName(dsName).asScala.toList
 
-          if (datasets.isEmpty || !userHasReadAccess(ctx, datasets.head.getDid, uid))
-            throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+          if (models.isEmpty || !userHasReadAccess(ctx, models.head.getMid, uid))
+            throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
 
-          val dataset = datasets.head
+          val model = models.head
           // Standard read access check only - download restrictions handled per endpoint
-          // Non-download operations (viewing) should work for all public datasets
+          // Non-download operations (viewing) should work for all public models
 
           (dsName, commit, decodedPathStr)
         }
         Right(response)
 
       case (None, None) =>
-        // Case 3: Neither datasetName nor commitHash are provided, resolve normally
+        // Case 3: Neither modelName nor commitHash are provided, resolve normally
         val response = withTransaction(context) { ctx =>
           val fileUri = FileResolver.resolve(decodedPathStr)
           val document = DocumentFactory.openReadonlyDocument(fileUri).asInstanceOf[OnDataset]
-          val datasetDao = new DatasetDao(ctx.configuration())
-          val datasets = datasetDao.fetchByName(document.getDatasetName()).asScala.toList
-
-          if (datasets.isEmpty || !userHasReadAccess(ctx, datasets.head.getDid, uid))
-            throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
+          val modelDao = new ModelDao(ctx.configuration())
+          val models = modelDao.fetchByName(document.getDatasetName()).asScala.toList
 
-          val dataset = datasets.head
+          if (models.isEmpty || !userHasReadAccess(ctx, models.head.getMid, uid))
+            throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
+          
           // Standard read access check only - download restrictions handled per endpoint
-          // Non-download operations (viewing) should work for all public datasets
+          // Non-download operations (viewing) should work for all public models
 
           (
             document.getDatasetName(),
diff --git a/core/gui/proxy.config.json b/core/gui/proxy.config.json
index 3acc9a480c..33df99be3d 100755
--- a/core/gui/proxy.config.json
+++ b/core/gui/proxy.config.json
@@ -9,6 +9,11 @@
     "secure": false,
     "changeOrigin": true
   },
+  "/api/model": {
+    "target": "http://localhost:9092",
+    "secure": false,
+    "changeOrigin": true
+  },
   "/api/access/dataset/**": {
     "target": "http://localhost:9092",
     "secure": false,
diff --git a/core/gui/src/app/app-routing.constant.ts b/core/gui/src/app/app-routing.constant.ts
index a22f6439e2..db50133d66 100644
--- a/core/gui/src/app/app-routing.constant.ts
+++ b/core/gui/src/app/app-routing.constant.ts
@@ -34,6 +34,7 @@ export const DASHBOARD_USER_PROJECT = `${DASHBOARD_USER}/project`;
 export const DASHBOARD_USER_WORKSPACE = `${DASHBOARD_USER}/workflow`;
 export const DASHBOARD_USER_WORKFLOW = `${DASHBOARD_USER}/workflow`;
 export const DASHBOARD_USER_DATASET = `${DASHBOARD_USER}/dataset`;
+export const DASHBOARD_USER_MODEL = `${DASHBOARD_USER}/model`;
 export const DASHBOARD_USER_DATASET_CREATE = `${DASHBOARD_USER_DATASET}/create`;
 export const DASHBOARD_USER_QUOTA = `${DASHBOARD_USER}/quota`;
 export const DASHBOARD_USER_DISCUSSION = `${DASHBOARD_USER}/discussion`;
diff --git a/core/gui/src/app/app-routing.module.ts b/core/gui/src/app/app-routing.module.ts
index fffe3f8e42..f8472bd57e 100644
--- a/core/gui/src/app/app-routing.module.ts
+++ b/core/gui/src/app/app-routing.module.ts
@@ -44,6 +44,7 @@ import { inject } from "@angular/core";
 import { GuiConfigService } from "./common/service/gui-config.service";
 import { Router, CanActivateFn } from "@angular/router";
 import { DASHBOARD_ABOUT } from "./app-routing.constant";
+import { UserModelComponent } from "./dashboard/component/user/user-model/user-model.component";
 
 const rootRedirectGuard: CanActivateFn = () => {
   const config = inject(GuiConfigService);
@@ -127,6 +128,10 @@ routes.push({
           path: "dataset",
           component: UserDatasetComponent,
         },
+        {
+          path: "model",
+          component: UserModelComponent,
+        },
         {
           path: "dataset/:did",
           component: DatasetDetailComponent,
diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts
index 13759e6e55..4b07f986a3 100644
--- a/core/gui/src/app/app.module.ts
+++ b/core/gui/src/app/app.module.ts
@@ -172,6 +172,13 @@ import { AdminSettingsComponent } from "./dashboard/component/admin/settings/adm
 import { catchError, of } from "rxjs";
 import { FormlyRepeatDndComponent } from "./common/formly/repeat-dnd/repeat-dnd.component";
 import { NzInputNumberModule } from "ng-zorro-antd/input-number";
+import { UserModelComponent } from "./dashboard/component/user/user-model/user-model.component";
+import {
+  UserModelVersionCreatorComponent
+} from "./dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component";
+import {
+  ModelDetailComponent
+} from "./dashboard/component/user/user-model/user-dataset-explorer/model-detail.component";
 
 registerLocaleData(en);
 
@@ -233,6 +240,9 @@ registerLocaleData(en);
     UserDatasetListItemComponent,
     UserDatasetFileRendererComponent,
     UserDatasetStagedObjectsListComponent,
+    UserModelVersionCreatorComponent,
+    UserModelComponent,
+    ModelDetailComponent,
     NzModalCommentBoxComponent,
     LeftPanelComponent,
     LocalLoginComponent,
diff --git a/core/gui/src/app/common/type/dataset.ts b/core/gui/src/app/common/type/dataset.ts
index 7825ca2797..44642d441a 100644
--- a/core/gui/src/app/common/type/dataset.ts
+++ b/core/gui/src/app/common/type/dataset.ts
@@ -39,3 +39,15 @@ export interface Dataset {
   description: string;
   creationTime: number | undefined;
 }
+
+export interface Model {
+  mid: number | undefined;
+  ownerUid: number | undefined;
+  name: string;
+  isPublic: boolean;
+  isDownloadable: boolean;
+  storagePath: string | undefined;
+  description: string;
+  creationTime: number | undefined;
+}
+
diff --git a/core/gui/src/app/common/type/gui-config.ts b/core/gui/src/app/common/type/gui-config.ts
index d60ba1cf01..7909843a6c 100644
--- a/core/gui/src/app/common/type/gui-config.ts
+++ b/core/gui/src/app/common/type/gui-config.ts
@@ -49,6 +49,7 @@ export interface SidebarTabs {
   projects_enabled: boolean;
   workflows_enabled: boolean;
   datasets_enabled: boolean;
+  models_enabled:boolean;
   quota_enabled: boolean;
   forum_enabled: boolean;
   about_enabled: boolean;
diff --git a/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.ts b/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.ts
index 863ea67b81..0955a69323 100644
--- a/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.ts
+++ b/core/gui/src/app/dashboard/component/admin/settings/admin-settings.component.ts
@@ -43,6 +43,7 @@ export class AdminSettingsComponent implements OnInit {
     projects_enabled: false,
     workflows_enabled: false,
     datasets_enabled: false,
+    models_enabled: false,
     quota_enabled: false,
     forum_enabled: false,
     about_enabled: false,
diff --git a/core/gui/src/app/dashboard/component/dashboard.component.html b/core/gui/src/app/dashboard/component/dashboard.component.html
index cf490d3304..92cae29991 100644
--- a/core/gui/src/app/dashboard/component/dashboard.component.html
+++ b/core/gui/src/app/dashboard/component/dashboard.component.html
@@ -97,6 +97,18 @@
               nzType="database"></span>
             <span>Datasets</span>
           </li>
+          <li
+            *ngIf="sidebarTabs.models_enabled"
+            nz-menu-item
+            nz-tooltip="Look up for models"
+            nzMatchRouter="true"
+            nzTooltipPlacement="right"
+            [routerLink]="DASHBOARD_USER_MODEL">
+            <span
+              nz-icon
+              nzType="database"></span>
+            <span>Models</span>
+          </li>
           <li
             *ngIf="sidebarTabs.quota_enabled"
             nz-menu-item
diff --git a/core/gui/src/app/dashboard/component/dashboard.component.ts b/core/gui/src/app/dashboard/component/dashboard.component.ts
index c07a78f7bf..a6f57c1545 100644
--- a/core/gui/src/app/dashboard/component/dashboard.component.ts
+++ b/core/gui/src/app/dashboard/component/dashboard.component.ts
@@ -38,7 +38,7 @@ import {
   DASHBOARD_USER_DISCUSSION,
   DASHBOARD_USER_PROJECT,
   DASHBOARD_USER_QUOTA,
-  DASHBOARD_USER_WORKFLOW,
+  DASHBOARD_USER_WORKFLOW, DASHBOARD_USER_MODEL,
 } from "../../app-routing.constant";
 import { Version } from "../../../environments/version";
 import { SidebarTabs } from "../../common/type/gui-config";
@@ -70,6 +70,7 @@ export class DashboardComponent implements OnInit {
     projects_enabled: false,
     workflows_enabled: false,
     datasets_enabled: false,
+    models_enabled: false,
     quota_enabled: false,
     forum_enabled: false,
     about_enabled: false,
@@ -78,6 +79,7 @@ export class DashboardComponent implements OnInit {
   protected readonly DASHBOARD_USER_PROJECT = DASHBOARD_USER_PROJECT;
   protected readonly DASHBOARD_USER_WORKFLOW = DASHBOARD_USER_WORKFLOW;
   protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET;
+  protected readonly DASHBOARD_USER_MODEL = DASHBOARD_USER_MODEL;
   protected readonly DASHBOARD_USER_QUOTA = DASHBOARD_USER_QUOTA;
   protected readonly DASHBOARD_USER_DISCUSSION = DASHBOARD_USER_DISCUSSION;
   protected readonly DASHBOARD_ADMIN_USER = DASHBOARD_ADMIN_USER;
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/model-detail.component.html b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/model-detail.component.html
new file mode 100644
index 0000000000..e0bee86403
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/model-detail.component.html
@@ -0,0 +1,336 @@
+<!--
+ 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.
+-->
+
+<div *ngIf="!isMaximized">
+  <nz-card>
+    <h2 class="dataset-title">Dataset: {{datasetName}}</h2>
+    <nz-card-meta
+      style="padding-top: 10px"
+      nzDescription="Created at: {{datasetCreationTime}}"></nz-card-meta>
+    <nz-card-meta
+      style="padding-top: 20px"
+      nzDescription="{{datasetDescription}}"></nz-card-meta>
+    <div
+      class="workflow-panel"
+      style="padding-top: 30px">
+      <div
+        *ngIf="!(userDatasetAccessLevel === 'NONE')"
+        class="dataset-controls">
+        <div class="control-row">
+          <label>Visibility:</label>
+          <nz-switch
+            [ngModel]="datasetIsPublic"
+            (ngModelChange)="onPublicStatusChange($event)"
+            [nzDisabled]="userDatasetAccessLevel !== 'WRITE'"
+            nzCheckedChildren="public"
+            nzUnCheckedChildren="private"></nz-switch>
+        </div>
+        <div
+          class="control-row"
+          *ngIf="isOwner">
+          <label>Download:</label>
+          <nz-switch
+            [ngModel]="datasetIsDownloadable"
+            (ngModelChange)="onDownloadableStatusChange($event)"
+            nzCheckedChildren="allowed"
+            nzUnCheckedChildren="blocked"></nz-switch>
+        </div>
+      </div>
+      <button
+        nz-button
+        nzType="text"
+        class="dropdown-item like-button"
+        title="View"
+        (click)="changeViewDisplayStyle()">
+        <i
+          nz-icon
+          [nzType]="'eye'"
+          class="like-icon">
+        </i>
+        <span>{{ formatCount(viewCount) }}</span>
+      </button>
+      <button
+        nz-button
+        nzType="text"
+        class="like-button"
+        title="Like"
+        [disabled]="!isLogin"
+        (click)="toggleLike()">
+        <i
+          nz-icon
+          [nzType]="'like'"
+          [ngClass]="{liked: isLiked}">
+        </i>
+        <span>{{ formatCount(likeCount) }}</span>
+      </button>
+    </div>
+  </nz-card>
+</div>
+<nz-layout>
+  <nz-content
+    [ngClass]="{'grayed-out': false, 'disabled-click': false}"
+    style="background-color: white">
+    <nz-card>
+      <div style="display: flex; justify-content: space-between; align-items: center">
+        <div class="file-info">
+          <h3 class="file-title">
+            <b>{{currentDisplayedFileName}}</b>
+            <span
+              *ngIf="currentFileSize"
+              class="file-size">
+              <i
+                nz-icon
+                nzType="file"
+                nzTheme="outline"
+                class="icon-file"></i>
+              {{ formatSize(currentFileSize) }}
+            </span>
+          </h3>
+        </div>
+        <div style="display: flex">
+          <button
+            nz-button
+            *ngIf="selectedVersion"
+            nz-tooltip="Download the file"
+            [disabled]="!isLogin || !isDownloadAllowed()"
+            (click)="onClickDownloadCurrentFile()">
+            <i
+              nz-icon
+              nzTheme="outline"
+              nzType="download">
+            </i>
+          </button>
+          <button
+            nz-button
+            *ngIf="!isMaximized && selectedVersion"
+            nz-tooltip="Maximize View"
+            (click)="onClickScaleTheView()">
+            <i
+              nz-icon
+              nzTheme="outline"
+              nzType="expand">
+            </i>
+          </button>
+          <button
+            nz-button
+            *ngIf="isMaximized && selectedVersion"
+            nz-tooltip="Minimize View"
+            (click)="onClickScaleTheView()">
+            <i
+              nz-icon
+              nzTheme="outline"
+              nzType="compress">
+            </i>
+          </button>
+          <button
+            *ngIf="!isRightBarCollapsed"
+            nz-button
+            nz-tooltip="Hide the right bar"
+            (click)="onClickHideRightBar()">
+            <i
+              nz-icon
+              nzTheme="outline"
+              nzType="right">
+            </i>
+          </button>
+          <button
+            *ngIf="isRightBarCollapsed"
+            nz-button
+            nz-tooltip="Show Tree"
+            (click)="onClickHideRightBar()">
+            <i
+              nz-icon
+              nzTheme="outline"
+              nzType="left">
+            </i>
+          </button>
+        </div>
+      </div>
+    </nz-card>
+    <nz-empty
+      class="empty-version-indicator"
+      *ngIf="!selectedVersion"
+      nzNotFoundContent="No version is selected"></nz-empty>
+
+    <texera-user-dataset-file-renderer
+      *ngIf="selectedVersion"
+      [isMaximized]="isMaximized"
+      [did]="did"
+      [dvid]="selectedVersion?.dvid"
+      [filePath]="currentDisplayedFileName"
+      [fileSize]="currentFileSize"
+      [isLogin]="isLogin"
+      class="file-renderer">
+    </texera-user-dataset-file-renderer>
+  </nz-content>
+  <nz-sider
+    *ngIf="!isRightBarCollapsed"
+    nzTheme="light"
+    style="float: right; height: 100%"
+    [nzWidth]="siderWidth"
+    nz-resizable
+    [nzMinWidth]="MIN_SIDER_WIDTH"
+    [nzMaxWidth]="MAX_SIDER_WIDTH"
+    (nzResize)="onSideResize($event)">
+    <nz-resize-handle nzDirection="left">
+      <div class="sider-resize-line">
+        <i
+          class="sider-resize-handle"
+          nz-icon
+          nzType="more"
+          nzTheme="outline"></i>
+      </div>
+    </nz-resize-handle>
+    <div class="right-sider">
+      <nz-collapse nzGhost>
+        <nz-collapse-panel
+          nzHeader="Current Versions"
+          nzActive="true">
+          <div class="version-storage">
+            <h6 style="font-weight: lighter; font-size: 0.9em">Choose a Version:</h6>
+            <div class="select-and-button-container">
+              <nz-select
+                nzShowSearch
+                nzAllowClear
+                nzPlaceHolder="Select a version"
+                (ngModelChange)="onVersionSelected($event)"
+                [(ngModel)]="selectedVersion">
+                <nz-option
+                  *ngFor="let version of versions"
+                  [nzValue]="version"
+                  [nzLabel]="version.name"></nz-option>
+              </nz-select>
+              <button
+                nz-button
+                nz-tooltip="Download Dataset"
+                (click)="onClickDownloadVersionAsZip()"
+                *ngIf="selectedVersion"
+                [disabled]="!isLogin || !isDownloadAllowed()"
+                class="spaced-button">
+                <i
+                  nz-icon
+                  nzType="download"
+                  nzTheme="outline"></i>
+              </button>
+            </div>
+            <div
+              *ngIf="selectedVersion"
+              class="version-size">
+              <i
+                nz-icon
+                nzType="database"
+                nzTheme="outline"
+                class="icon-database"></i>
+              Version Size: {{ formatSize(currentDatasetVersionSize) }}
+            </div>
+          </div>
+          <texera-user-dataset-version-filetree
+            [fileTreeNodes]="fileTreeNodeList"
+            [isTreeNodeDeletable]="true"
+            (selectedTreeNode)="onVersionFileTreeNodeSelected($event)"
+            (deletedTreeNode)="onPreviouslyUploadedFileDeleted($event)">
+          </texera-user-dataset-version-filetree>
+        </nz-collapse-panel>
+      </nz-collapse>
+
+      <nz-divider></nz-divider>
+      <nz-collapse
+        *ngIf="userDatasetAccessLevel === 'WRITE'"
+        nzGhost>
+        <nz-collapse-panel
+          nzActive="true"
+          nzHeader="Create New Version">
+          <texera-user-files-uploader (uploadedFiles)="onNewUploadFilesChanged($event)"> </texera-user-files-uploader>
+          <div class="upload-progress-wrapper">
+            <div
+              *ngFor="let task of uploadTasks; trackBy: trackByTask"
+              class="upload-progress-container">
+              <div class="progress-header">
+                <span><b>{{ task.status }}</b>: {{ task.filePath }}</span>
+                <button
+                  nz-button
+                  nzType="text"
+                  nzShape="circle"
+                  [nz-tooltip]="
+                (task.status === 'aborted' || task.status === 'finished')
+                ? 'Close'
+                : 'Cancel the upload'
+                "
+                  (click)="onClickAbortUploadProgress(task)">
+                  <i
+                    nz-icon
+                    nzType="close"
+                    nzTheme="outline"></i>
+                </button>
+              </div>
+              <div
+                class="upload-stats"
+                *ngIf="task.status !== 'initializing'">
+                <nz-progress
+                  [nzPercent]="task.percentage"
+                  [nzStatus]="getUploadStatus(task.status)"></nz-progress>
+                <nz-tag
+                  *ngIf="task.status === 'uploading'"
+                  [nzColor]="'blue'">
+                  <span class="fixed-width-speed">{{ formatSpeed(task.uploadSpeed) }}</span> -
+                  <span class="fixed-width-time">{{ formatTime(task.totalTime ?? 0) }}</span> elapsed,
+                  <span class="fixed-width-time">{{ formatTime(task.estimatedTimeRemaining ?? 0) }} left</span>
+                </nz-tag>
+
+                <nz-tag *ngIf="(task.status === 'finished' || task.status === 'aborted')">
+                  Upload time: {{ formatTime(task.totalTime ?? 0) }}
+                </nz-tag>
+              </div>
+            </div>
+          </div>
+
+          <texera-dataset-staged-objects-list
+            [uploadTimeMap]="uploadTimeMap"
+            [did]="did"
+            [userMakeChangesEvent]="userMakeChanges"
+            (stagedObjectsChanged)="onStagedObjectsUpdated($event)"></texera-dataset-staged-objects-list>
+          <div
+            *ngIf="userHasWriteAccess() && userHasPendingChanges"
+            class="version-creator">
+            <div class="version-input-container">
+              <label>Version:</label>
+              <input
+                nz-input
+                [(ngModel)]="versionName"
+                placeholder="Describe the new version (Optional)"
+                [disabled]="isCreatingVersion"
+                (keydown.enter)="onClickOpenVersionCreator()"
+                class="version-input" />
+            </div>
+            <div>
+              <button
+                nz-button
+                nzType="primary"
+                [nzLoading]="isCreatingVersion"
+                (click)="onClickOpenVersionCreator()"
+                class="create-dataset-version-button">
+                Submit
+              </button>
+            </div>
+          </div>
+        </nz-collapse-panel>
+      </nz-collapse>
+    </div>
+  </nz-sider>
+</nz-layout>
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/model-detail.component.scss b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/model-detail.component.scss
new file mode 100644
index 0000000000..68087749ae
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/model-detail.component.scss
@@ -0,0 +1,247 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.create-dataset-version-button {
+  display: flex; /* Use flexbox for centering */
+  align-items: center; /* Center vertically */
+  justify-content: center; /* Center horizontally */
+  color: white;
+  border: none;
+  padding: 12px 40px; /* Increase padding for a wider button */
+  border-radius: 25px;
+  cursor: pointer;
+  transition: background-color 0.3s;
+  margin: 18px auto 0 auto; /* Auto margins for horizontal centering */
+  width: 200px; /* Adjust width as needed */
+  font-size: 18px; /* Make text slightly bigger */
+  font-weight: bold; /* Optional: Make text bold */
+}
+
+.version-storage {
+  padding-left: 20px;
+  width: 90%;
+  margin-left: auto;
+  margin-right: auto;
+  margin-bottom: 25px;
+}
+
+.version-storage nz-select {
+  width: 100%; /* Make the select element take the full width of its parent */
+}
+
+.right-panel-title {
+  padding-left: 20px;
+  text-align: center;
+  margin-top: 10px;
+}
+
+nz-layout {
+  height: 100%;
+}
+
+html,
+body {
+  height: 100%;
+  margin: 0;
+  padding: 0;
+}
+
+.sider-resize-line {
+  height: 100%;
+  width: 5px;
+  border-right: 1px solid #e8e8e8;
+}
+
+.sider-resize-handle {
+  background: #fff;
+  border: 1px solid #ddd;
+  text-align: center;
+  font-size: 12px;
+  height: 20px;
+  line-height: 20px;
+  margin-top: 400px;
+}
+
+.file-renderer {
+  width: 95%;
+  height: 80%;
+  margin: auto;
+}
+
+.dataset-version-creator {
+  height: 100%;
+}
+
+.grayed-out {
+  filter: grayscale(1) opacity(0.7);
+  background-color: #f0f0f0; /* Adjust the color as needed */
+}
+
+.disabled-click {
+  pointer-events: none;
+}
+
+.select-and-button-container {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.spaced-button {
+  flex-shrink: 0;
+  width: 30px;
+  height: 24px;
+}
+
+nz-select {
+  flex-grow: 1;
+}
+
+.file-title {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.file-size,
+.version-size {
+  font-size: 12px;
+  color: #8c8c8c;
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.version-size {
+  margin-top: 8px;
+}
+
+.icon-database,
+.icon-file {
+  font-size: 14px;
+}
+
+.version-size-badge {
+  margin-top: 10px;
+}
+
+.nz-tag {
+  font-size: 12px;
+}
+
+.workflow-panel {
+  align-items: center;
+  gap: 10px;
+}
+
+.like-button {
+  margin-left: 15px;
+  width: 70px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.liked {
+  color: red;
+}
+
+.empty-version-indicator {
+  margin-top: 15%;
+}
+
+.upload-progress-wrapper {
+  max-height: 25vh;
+  overflow-y: auto;
+  padding-right: 4px;
+}
+
+.upload-progress-container {
+  margin-left: 20px;
+}
+
+.version-creator {
+  margin-top: 20px;
+  padding: 40px;
+}
+
+.version-input-container label {
+  font-size: 15px;
+}
+
+.version-input-container {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.version-input {
+  padding: 6px;
+}
+
+.dataset-controls {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  margin-right: 20px;
+  margin-bottom: 20px;
+}
+
+.control-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+
+  label {
+    min-width: 80px;
+    font-weight: 500;
+    color: #666;
+  }
+
+  nz-switch {
+    margin-left: 8px;
+  }
+
+  .help-text {
+    margin-left: 8px;
+    font-size: 12px;
+    color: #999;
+    font-style: italic;
+  }
+}
+
+.upload-stats {
+  font-size: 13px;
+  margin-bottom: 20px;
+}
+
+:host ::ng-deep .upload-stats .ant-tag {
+  border: none;
+}
+
+.fixed-width-speed {
+  display: inline-block;
+  min-width: 5ch;
+  text-align: right;
+}
+
+.fixed-width-time {
+  display: inline-block;
+  min-width: 2ch;
+  text-align: right;
+}
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/model-detail.component.ts b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/model-detail.component.ts
new file mode 100644
index 0000000000..0762021f0b
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/model-detail.component.ts
@@ -0,0 +1,567 @@
+/**
+ * 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 { Component, EventEmitter, OnInit, Output } from "@angular/core";
+import { ActivatedRoute } from "@angular/router";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { DatasetService, MultipartUploadProgress } from "../../../../service/user/dataset/dataset.service";
+import { NzResizeEvent } from "ng-zorro-antd/resizable";
+import {
+  DatasetFileNode,
+  getFullPathFromDatasetFileNode,
+  getRelativePathFromDatasetFileNode,
+} from "../../../../../common/type/datasetVersionFileTree";
+import { DatasetVersion } from "../../../../../common/type/dataset";
+import { switchMap, throttleTime } from "rxjs/operators";
+import { NotificationService } from "../../../../../common/service/notification/notification.service";
+import { DownloadService } from "../../../../service/user/download/download.service";
+import { formatSize } from "src/app/common/util/size-formatter.util";
+import { UserService } from "../../../../../common/service/user/user.service";
+import { isDefined } from "../../../../../common/util/predicate";
+import { ActionType, EntityType, HubService, LikedStatus } from "../../../../../hub/service/hub.service";
+import { FileUploadItem } from "../../../../type/dashboard-file.interface";
+import { DatasetStagedObject } from "../../../../../common/type/dataset-staged-object";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { UserModelVersionCreatorComponent } from "./user-dataset-version-creator/user-model-version-creator.component";
+import { AdminSettingsService } from "../../../../service/admin/settings/admin-settings.service";
+import { HttpErrorResponse } from "@angular/common/http";
+import { Subscription } from "rxjs";
+import { formatSpeed, formatTime } from "src/app/common/util/format.util";
+
+export const THROTTLE_TIME_MS = 1000;
+
+@UntilDestroy()
+@Component({
+  templateUrl: "./model-detail.component.html",
+  styleUrls: ["./model-detail.component.scss"],
+})
+export class ModelDetailComponent implements OnInit {
+  public did: number | undefined;
+  public datasetName: string = "";
+  public datasetDescription: string = "";
+  public datasetCreationTime: string = "";
+  public datasetIsPublic: boolean = false;
+  public datasetIsDownloadable: boolean = true;
+  public userDatasetAccessLevel: "READ" | "WRITE" | "NONE" = "NONE";
+  public isOwner: boolean = false;
+
+  public currentDisplayedFileName: string = "";
+  public currentFileSize: number | undefined;
+  public currentDatasetVersionSize: number | undefined;
+
+  public isRightBarCollapsed = false;
+  public isMaximized = false;
+
+  public versions: ReadonlyArray<DatasetVersion> = [];
+  public selectedVersion: DatasetVersion | undefined;
+  public fileTreeNodeList: DatasetFileNode[] = [];
+
+  public versionCreatorBaseVersion: DatasetVersion | undefined;
+  public isLogin: boolean = this.userService.isLogin();
+
+  public isLiked: boolean = false;
+  public likeCount: number = 0;
+  public currentUid: number | undefined;
+  public viewCount: number = 0;
+  public displayPreciseViewCount = false;
+
+  userHasPendingChanges: boolean = false;
+  // Uploading setting
+  chunkSizeMB: number = 50;
+  maxConcurrentChunks: number = 10;
+  private uploadSubscriptions = new Map<string, Subscription>();
+  uploadTimeMap = new Map<string, number>();
+
+  versionName: string = "";
+  isCreatingVersion: boolean = false;
+
+  //  List of upload tasks – each task tracked by its filePath
+  public uploadTasks: Array<
+    MultipartUploadProgress & {
+      filePath: string;
+    }
+  > = [];
+  private autoHideTimers: number[] = [];
+
+  @Output() userMakeChanges = new EventEmitter<void>();
+
+  constructor(
+    private route: ActivatedRoute,
+    private modalService: NzModalService,
+    private datasetService: DatasetService,
+    private notificationService: NotificationService,
+    private downloadService: DownloadService,
+    private userService: UserService,
+    private hubService: HubService,
+    private adminSettingsService: AdminSettingsService
+  ) {
+    this.userService
+      .userChanged()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        this.currentUid = this.userService.getCurrentUser()?.uid;
+        this.isLogin = this.userService.isLogin();
+      });
+  }
+
+  // item for control the resizeable sider
+  MAX_SIDER_WIDTH = 600;
+  MIN_SIDER_WIDTH = 150;
+  siderWidth = 400;
+  id = -1;
+  onSideResize({ width }: NzResizeEvent): void {
+    cancelAnimationFrame(this.id);
+    this.id = requestAnimationFrame(() => {
+      this.siderWidth = width!;
+    });
+  }
+
+  ngOnInit(): void {
+    this.route.params
+      .pipe(
+        switchMap(params => {
+          this.did = params["did"];
+          this.retrieveDatasetInfo();
+          this.retrieveDatasetVersionList();
+          return this.route.data; // or some other observable
+        }),
+        untilDestroyed(this)
+      )
+      .subscribe();
+
+    if (!isDefined(this.did)) {
+      return;
+    }
+
+    this.hubService
+      .getCounts([EntityType.Dataset], [this.did], [ActionType.Like])
+      .pipe(untilDestroyed(this))
+      .subscribe(counts => {
+        this.likeCount = counts[0].counts.like ?? 0;
+      });
+
+    this.hubService
+      .postView(this.did, this.currentUid ? this.currentUid : 0, EntityType.Dataset)
+      .pipe(throttleTime(THROTTLE_TIME_MS))
+      .pipe(untilDestroyed(this))
+      .subscribe(count => {
+        this.viewCount = count;
+      });
+
+    if (!isDefined(this.currentUid)) {
+      return;
+    }
+
+    this.hubService
+      .isLiked([this.did], [EntityType.Dataset])
+      .pipe(untilDestroyed(this))
+      .subscribe((isLiked: LikedStatus[]) => {
+        this.isLiked = isLiked.length > 0 ? isLiked[0].isLiked : false;
+      });
+
+    this.loadUploadSettings();
+  }
+
+  public onClickOpenVersionCreator() {
+    if (this.did && !this.isCreatingVersion) {
+      this.isCreatingVersion = true;
+
+      this.datasetService
+        .createDatasetVersion(this.did, this.versionName?.trim() || "")
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: res => {
+            this.notificationService.success("Version Created");
+            this.isCreatingVersion = false;
+            this.versionName = "";
+            this.retrieveDatasetVersionList();
+            this.userMakeChanges.emit();
+          },
+          error: (res: unknown) => {
+            const err = res as HttpErrorResponse;
+            this.notificationService.error(`Version creation failed: ${err.error.message}`);
+            this.isCreatingVersion = false;
+          },
+        });
+    }
+  }
+
+  public onClickDownloadVersionAsZip() {
+    if (this.did && this.selectedVersion && this.selectedVersion.dvid) {
+      this.downloadService
+        .downloadDatasetVersion(this.did, this.selectedVersion.dvid, this.datasetName, this.selectedVersion.name)
+        .pipe(untilDestroyed(this))
+        .subscribe();
+    }
+  }
+
+  onPublicStatusChange(checked: boolean): void {
+    // Handle the change in dataset public status
+    if (this.did) {
+      this.datasetService
+        .updateDatasetPublicity(this.did)
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: (res: Response) => {
+            this.datasetIsPublic = checked;
+            let state = "public";
+            if (!this.datasetIsPublic) {
+              state = "private";
+            }
+            this.notificationService.success(`Dataset ${this.datasetName} is now ${state}`);
+          },
+          error: (err: unknown) => {
+            this.notificationService.error("Fail to change the dataset publicity");
+          },
+        });
+    }
+  }
+
+  onDownloadableStatusChange(checked: boolean): void {
+    // Handle the change in dataset downloadable status
+    if (this.did) {
+      this.datasetService
+        .updateDatasetDownloadable(this.did)
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: (res: Response) => {
+            this.datasetIsDownloadable = checked;
+            let state = "allowed";
+            if (!this.datasetIsDownloadable) {
+              state = "not allowed";
+            }
+            this.notificationService.success(`Dataset downloads are now ${state}`);
+          },
+          error: (err: unknown) => {
+            this.notificationService.error("Failed to change the dataset download permission");
+          },
+        });
+    }
+  }
+
+  retrieveDatasetInfo() {
+    if (this.did) {
+      this.datasetService
+        .getDataset(this.did, this.isLogin)
+        .pipe(untilDestroyed(this))
+        .subscribe(dashboardDataset => {
+          const dataset = dashboardDataset.dataset;
+          this.datasetName = dataset.name;
+          this.datasetDescription = dataset.description;
+          this.userDatasetAccessLevel = dashboardDataset.accessPrivilege;
+          this.datasetIsPublic = dataset.isPublic;
+          this.datasetIsDownloadable = dataset.isDownloadable;
+          this.isOwner = dashboardDataset.isOwner;
+          if (typeof dataset.creationTime === "number") {
+            this.datasetCreationTime = new Date(dataset.creationTime).toString();
+          }
+        });
+    }
+  }
+
+  retrieveDatasetVersionList() {
+    if (this.did) {
+      this.datasetService
+        .retrieveDatasetVersionList(this.did, this.isLogin)
+        .pipe(untilDestroyed(this))
+        .subscribe(versionNames => {
+          this.versions = versionNames;
+          // by default, the selected version is the 1st element in the retrieved list
+          // which is guaranteed(by the backend) to be the latest created version.
+          if (this.versions.length > 0) {
+            this.selectedVersion = this.versions[0];
+            this.onVersionSelected(this.selectedVersion);
+          }
+        });
+    }
+  }
+
+  loadFileContent(node: DatasetFileNode) {
+    this.currentDisplayedFileName = getFullPathFromDatasetFileNode(node);
+    this.currentFileSize = node.size;
+  }
+
+  onClickDownloadCurrentFile = (): void => {
+    if (!this.did || !this.selectedVersion?.dvid) return;
+    // For public datasets accessed by non-owners, use public endpoint
+    const shouldUsePublicEndpoint = this.datasetIsPublic && !this.isOwner;
+    this.downloadService
+      .downloadSingleFile(this.currentDisplayedFileName, !shouldUsePublicEndpoint)
+      .pipe(untilDestroyed(this))
+      .subscribe();
+  };
+
+  onClickScaleTheView() {
+    this.isMaximized = !this.isMaximized;
+  }
+
+  onClickHideRightBar() {
+    this.isRightBarCollapsed = !this.isRightBarCollapsed;
+  }
+
+  onStagedObjectsUpdated(stagedObjects: DatasetStagedObject[]) {
+    this.userHasPendingChanges = stagedObjects.length > 0;
+  }
+
+  onVersionSelected(version: DatasetVersion): void {
+    this.selectedVersion = version;
+    if (this.did && this.selectedVersion.dvid)
+      this.datasetService
+        .retrieveDatasetVersionFileTree(this.did, this.selectedVersion.dvid, this.isLogin)
+        .pipe(untilDestroyed(this))
+        .subscribe(data => {
+          this.fileTreeNodeList = data.fileNodes;
+          this.currentDatasetVersionSize = data.size;
+          let currentNode = this.fileTreeNodeList[0];
+          while (currentNode.type === "directory" && currentNode.children) {
+            currentNode = currentNode.children[0];
+          }
+          this.loadFileContent(currentNode);
+        });
+  }
+
+  onVersionFileTreeNodeSelected(node: DatasetFileNode) {
+    this.loadFileContent(node);
+  }
+
+  userHasWriteAccess(): boolean {
+    return this.userDatasetAccessLevel == "WRITE";
+  }
+
+  isDownloadAllowed(): boolean {
+    // Owners can always download
+    if (this.isOwner) {
+      return true;
+    }
+    // Non-owners can download if dataset is downloadable and they have access
+    // For public datasets, users have access even if userDatasetAccessLevel is 'NONE'
+    // For private datasets, users need explicit access (userDatasetAccessLevel !== 'NONE')
+    return this.datasetIsDownloadable && (this.datasetIsPublic || this.userDatasetAccessLevel !== "NONE");
+  }
+
+  // Track multiple file by unique key
+  trackByTask(_: number, task: MultipartUploadProgress & { filePath: string }): string {
+    return task.filePath;
+  }
+
+  private loadUploadSettings(): void {
+    this.adminSettingsService
+      .getSetting("multipart_upload_chunk_size_mb")
+      .pipe(untilDestroyed(this))
+      .subscribe(value => (this.chunkSizeMB = parseInt(value)));
+    this.adminSettingsService
+      .getSetting("max_number_of_concurrent_uploading_file_chunks")
+      .pipe(untilDestroyed(this))
+      .subscribe(value => (this.maxConcurrentChunks = parseInt(value)));
+  }
+
+  onNewUploadFilesChanged(files: FileUploadItem[]) {
+    if (this.did) {
+      files.forEach((file, idx) => {
+        // Cancel any existing upload for the same file to prevent progress confusion
+        this.uploadSubscriptions.get(file.name)?.unsubscribe();
+        this.uploadSubscriptions.delete(file.name);
+        this.uploadTasks = this.uploadTasks.filter(t => t.filePath !== file.name);
+
+        // Add an initializing task placeholder to uploadTasks
+        this.uploadTasks.push({
+          filePath: file.name,
+          percentage: 0,
+          status: "initializing",
+          uploadId: "",
+          physicalAddress: "",
+        });
+        // Start multipart upload
+        const subscription = this.datasetService
+          .multipartUpload(
+            this.datasetName,
+            file.name,
+            file.file,
+            this.chunkSizeMB * 1024 * 1024,
+            this.maxConcurrentChunks
+          )
+          .pipe(untilDestroyed(this))
+          .subscribe({
+            next: progress => {
+              // Find the task
+              const taskIndex = this.uploadTasks.findIndex(t => t.filePath === file.name);
+
+              if (taskIndex !== -1) {
+                // Update the task with new progress info
+                this.uploadTasks[taskIndex] = {
+                  ...this.uploadTasks[taskIndex],
+                  ...progress,
+                  percentage: progress.percentage ?? this.uploadTasks[taskIndex].percentage ?? 0,
+                };
+
+                // Auto‑hide when upload is truly finished
+                if (progress.status === "finished" && progress.totalTime) {
+                  const filename = file.name.split("/").pop() || file.name;
+                  this.uploadTimeMap.set(filename, progress.totalTime);
+                  this.userMakeChanges.emit();
+                  this.scheduleHide(taskIndex);
+                }
+              }
+            },
+            error: () => {
+              // Handle upload error
+              const taskIndex = this.uploadTasks.findIndex(t => t.filePath === file.name);
+
+              if (taskIndex !== -1) {
+                this.uploadTasks[taskIndex] = {
+                  ...this.uploadTasks[taskIndex],
+                  percentage: 100,
+                  status: "aborted",
+                };
+                this.scheduleHide(taskIndex);
+              }
+            },
+            complete: () => {
+              const taskIndex = this.uploadTasks.findIndex(t => t.filePath === file.name);
+              if (taskIndex !== -1 && this.uploadTasks[taskIndex].status !== "finished") {
+                this.uploadTasks[taskIndex].status = "finished";
+                this.userMakeChanges.emit();
+                this.scheduleHide(taskIndex);
+              }
+            },
+          });
+        // Store the subscription for later cleanup
+        this.uploadSubscriptions.set(file.name, subscription);
+      });
+    }
+  }
+
+  // Hide a task row after 5s (stores timer to clear on destroy) and clean up its subscription
+  private scheduleHide(idx: number) {
+    if (idx === -1) {
+      return;
+    }
+    const key = this.uploadTasks[idx].filePath;
+    this.uploadSubscriptions.delete(key);
+    const handle = window.setTimeout(() => {
+      this.uploadTasks = this.uploadTasks.filter(t => t.filePath !== key);
+    }, 5000);
+    this.autoHideTimers.push(handle);
+  }
+
+  onClickAbortUploadProgress(task: MultipartUploadProgress & { filePath: string }) {
+    const subscription = this.uploadSubscriptions.get(task.filePath);
+    if (subscription) {
+      subscription.unsubscribe();
+      this.uploadSubscriptions.delete(task.filePath);
+    }
+    this.datasetService
+      .finalizeMultipartUpload(
+        this.datasetName,
+        task.filePath,
+        task.uploadId,
+        [],
+        task.physicalAddress,
+        true // abort flag
+      )
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        this.notificationService.info(`${task.filePath} uploading has been terminated`);
+      });
+    // Remove the aborted task immediately
+    this.uploadTasks = this.uploadTasks.filter(t => t.filePath !== task.filePath);
+  }
+
+  getUploadStatus(status: "initializing" | "uploading" | "finished" | "aborted"): "active" | "exception" | "success" {
+    return status === "uploading" || status === "initializing"
+      ? "active"
+      : status === "aborted"
+        ? "exception"
+        : "success";
+  }
+
+  onPreviouslyUploadedFileDeleted(node: DatasetFileNode) {
+    if (this.did) {
+      this.datasetService
+        .deleteDatasetFile(this.did, getRelativePathFromDatasetFileNode(node))
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: (res: Response) => {
+            this.notificationService.success(
+              `File ${node.name} is successfully deleted. You may finalize it or revert it at the "Create Version" panel`
+            );
+            this.userMakeChanges.emit();
+          },
+          error: (err: unknown) => {
+            this.notificationService.error("Failed to delete the file");
+          },
+        });
+    }
+  }
+
+  // alias for formatSize
+  formatSize = formatSize;
+
+  formatCount(count: number): string {
+    if (count >= 1000) {
+      return (count / 1000).toFixed(1) + "k";
+    }
+    return count.toString();
+  }
+  formatTime = formatTime;
+  formatSpeed = formatSpeed;
+
+  toggleLike(): void {
+    const userId = this.currentUid;
+    if (!isDefined(userId) || !isDefined(this.did)) {
+      return;
+    }
+
+    if (this.isLiked) {
+      this.hubService
+        .postUnlike(this.did, EntityType.Dataset)
+        .pipe(untilDestroyed(this))
+        .subscribe((success: boolean) => {
+          if (success) {
+            this.isLiked = false;
+            this.hubService
+              .getCounts([EntityType.Dataset], [this.did!], [ActionType.Like])
+              .pipe(untilDestroyed(this))
+              .subscribe(counts => {
+                this.likeCount = counts[0].counts.like ?? 0;
+              });
+          }
+        });
+    } else {
+      this.hubService
+        .postLike(this.did, EntityType.Dataset)
+        .pipe(untilDestroyed(this))
+        .subscribe((success: boolean) => {
+          if (success) {
+            this.isLiked = true;
+            this.hubService
+              .getCounts([EntityType.Dataset], [this.did!], [ActionType.Like])
+              .pipe(untilDestroyed(this))
+              .subscribe(counts => {
+                this.likeCount = counts[0].counts.like ?? 0;
+              });
+          }
+        });
+    }
+  }
+
+  changeViewDisplayStyle() {
+    this.displayPreciseViewCount = !this.displayPreciseViewCount;
+  }
+}
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.html b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.html
new file mode 100644
index 0000000000..fd0ba3af15
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.html
@@ -0,0 +1,104 @@
+<!--
+ 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.
+-->
+
+<div
+  [ngStyle]="!isMaximized ? {'padding-top': '15px', 'margin-left': '30px', 'width': '95%', 'height': '80%', 'overflow-x': 'auto', 'overflow-y': 'auto'}:
+                                {'padding-top': '15px', 'margin-left': '30px', 'width': '95%', 'height': '100%', 'overflow-x': 'auto', 'overflow-y': 'auto'}">
+  <div class="file-display-area">
+    <div *ngIf="isLoading">
+      <nz-spin nzTip="Loading...">
+        <nz-alert
+          nzType="info"
+          nzMessage="File content is loading"></nz-alert>
+      </nz-spin>
+    </div>
+    <div *ngIf="isFileLoadingError">
+      <nz-alert
+        nzType="error"
+        nzMessage="File loading encounter error."
+        nzDescription="Ops, There is something wrong when loading the file you are requesting."></nz-alert>
+    </div>
+    <div *ngIf="isFileSizeUnloadable">
+      <nz-alert
+        nzType="warning"
+        nzMessage="File is too large to preview"></nz-alert>
+    </div>
+    <div *ngIf="isFileTypePreviewUnsupported">
+      <nz-alert
+        nzType="warning"
+        nzMessage="Preview of the file type is currently not supported"></nz-alert>
+    </div>
+
+    <nz-table
+      *ngIf="displayCSV || displayXlsx"
+      #basicTable
+      [nzData]="tableContent">
+      <thead>
+        <tr>
+          <th *ngFor="let column of tableDataHeader">{{ column }}</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr *ngFor="let row of basicTable.data">
+          <td *ngFor="let cell of row">{{ cell }}</td>
+        </tr>
+      </tbody>
+    </nz-table>
+
+    <div *ngIf="displayImage && safeFileURL">
+      <img
+        [src]="safeFileURL"
+        alt="{{filePath}}"
+        (click)="toggleImageModal()"
+        style="width: 50%; height: 70%; margin: auto" />
+    </div>
+
+    <!-- Full-size image modal -->
+    <div
+      *ngIf="showImageModal"
+      class="image-modal"
+      (click)="toggleImageModal()">
+      <img
+        [src]="safeFileURL"
+        alt="{{filePath}}"
+        class="full-size-image" />
+    </div>
+    <div *ngIf="displayMarkdown">
+      <markdown [data]="textContent"></markdown>
+    </div>
+    <div *ngIf="displayJson">
+      <ngx-json-viewer [json]="textContent"></ngx-json-viewer>
+    </div>
+    <div *ngIf="displayMP4 && safeFileURL">
+      <video
+        controls
+        [src]="safeFileURL">
+        Your browser does not support the video tag.
+      </video>
+    </div>
+    <div *ngIf="displayMP3 && safeFileURL">
+      <audio
+        controls
+        [src]="safeFileURL">
+        Your browser does not support the audio element.
+      </audio>
+    </div>
+    <div *ngIf="displayPlainText">{{ textContent }}</div>
+  </div>
+</div>
diff --git a/core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.scss
similarity index 69%
copy from core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts
copy to core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.scss
index 2ea0058264..4b5865edaf 100644
--- a/core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.scss
@@ -17,13 +17,27 @@
  * under the License.
  */
 
-import { Dataset, DatasetVersion } from "../../common/type/dataset";
-import { DatasetFileNode } from "../../common/type/datasetVersionFileTree";
+.file-display-area {
+  width: 90%;
+  height: 100%;
+  margin: auto;
+}
+
+/* CSS styles */
+.image-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.8);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 1000;
+}
 
-export interface DashboardDataset {
-  isOwner: boolean;
-  ownerEmail: string;
-  dataset: Dataset;
-  accessPrivilege: "READ" | "WRITE" | "NONE";
-  size: number;
+.full-size-image {
+  max-width: 90%;
+  max-height: 90%;
 }
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.spec.ts b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.spec.ts
new file mode 100644
index 0000000000..0062a9a328
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.spec.ts
@@ -0,0 +1,57 @@
+/**
+ * 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 { TestBed } from "@angular/core/testing";
+import { HttpClientTestingModule } from "@angular/common/http/testing";
+import { UserDatasetFileRendererComponent } from "./user-dataset-file-renderer.component";
+import { DatasetService } from "../../../../../service/user/dataset/dataset.service";
+import { NotificationService } from "../../../../../../common/service/notification/notification.service";
+import { DomSanitizer } from "@angular/platform-browser";
+import { commonTestProviders } from "../../../../../../common/testing/test-utils";
+
+describe("UserDatasetFileRendererComponent", () => {
+  let component: UserDatasetFileRendererComponent;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule],
+      declarations: [UserDatasetFileRendererComponent],
+      providers: [
+        DatasetService,
+        NotificationService,
+        { provide: DomSanitizer, useValue: jasmine.createSpyObj("DomSanitizer", ["bypassSecurityTrustUrl"]) },
+        ...commonTestProviders,
+      ],
+    });
+    const fixture = TestBed.createComponent(UserDatasetFileRendererComponent);
+    component = fixture.componentInstance;
+  });
+
+  it("should return true for supported MIME type", () => {
+    const supportedMimeType = "image/jpeg"; // Example of a supported MIME type
+    const result = component.isPreviewSupported(supportedMimeType);
+    expect(result).toBeTrue();
+  });
+
+  it("should return false for unsupported MIME type", () => {
+    const unsupportedMimeType = "application/unknown"; // Example of an unsupported MIME type
+    const result = component.isPreviewSupported(unsupportedMimeType);
+    expect(result).toBeFalse();
+  });
+});
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts
new file mode 100644
index 0000000000..b56c1e6d44
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component.ts
@@ -0,0 +1,347 @@
+/**
+ * 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 { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, OnDestroy } from "@angular/core";
+import { DatasetService } from "../../../../../service/user/dataset/dataset.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import * as Papa from "papaparse";
+import { ParseResult } from "papaparse";
+import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
+import readXlsxFile from "read-excel-file";
+import { NotificationService } from "../../../../../../common/service/notification/notification.service";
+
+export const MIME_TYPES = {
+  JPEG: "image/jpeg",
+  JPG: "image/jpeg",
+  PNG: "image/png",
+  CSV: "text/csv",
+  TXT: "text/plain",
+  MD: "text/markdown",
+  HTML: "text/html",
+  JSON: "application/json",
+  PDF: "application/pdf",
+  MSWORD: "application/msword",
+  MSEXCEL: "application/vnd.ms-excel",
+  MSPOWERPOINT: "application/vnd.ms-powerpoint",
+  MP4: "video/mp4",
+  MP3: "audio/mpeg",
+  OCTET_STREAM: "application/octet-stream", // Default binary format
+};
+
+export function getMimeType(filename: string): string {
+  const extension = filename.split(".").pop()?.toUpperCase();
+  return extension && MIME_TYPES[extension as keyof typeof MIME_TYPES]
+    ? MIME_TYPES[extension as keyof typeof MIME_TYPES]
+    : MIME_TYPES.OCTET_STREAM;
+}
+
+// the size limits for all preview-supported types
+export const MIME_TYPE_SIZE_LIMITS_MB = {
+  [MIME_TYPES.JPEG]: 5 * 1024 * 1024, // 5 MB
+  [MIME_TYPES.PNG]: 5 * 1024 * 1024, // 5 MB
+  [MIME_TYPES.CSV]: 2 * 1024 * 1024, // 2 MB for text-based data files
+  [MIME_TYPES.TXT]: 1 * 1024 * 1024, // 1 MB for plain text files
+  [MIME_TYPES.MD]: 1 * 1024 * 1024, // 1 MB for MD files
+  [MIME_TYPES.JSON]: 1 * 1024 * 1024, // 1 MB for JSON files
+  [MIME_TYPES.MSEXCEL]: 10 * 1024 * 1024, // 10 MB for Excel spreadsheets
+  [MIME_TYPES.MP4]: 50 * 1024 * 1024, // 50 MB for MP4 videos
+  [MIME_TYPES.MP3]: 10 * 1024 * 1024, // 10 MB for MP3 audio files
+  [MIME_TYPES.OCTET_STREAM]: 5 * 1024 * 1024, // Default size for other binary formats
+};
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-dataset-file-renderer",
+  templateUrl: "./user-dataset-file-renderer.component.html",
+  styleUrls: ["./user-dataset-file-renderer.component.scss"],
+})
+export class UserDatasetFileRendererComponent implements OnInit, OnChanges, OnDestroy {
+  private DEFAULT_MAX_SIZE = 5 * 1024 * 1024; // 5 MB
+
+  public fileURL: string | undefined;
+  // safe url is used to display some formats including image
+  public safeFileURL: SafeUrl | undefined;
+
+  // table related control
+  public displayCSV: boolean = false;
+  public displayXlsx: boolean = false;
+  public tableDataHeader: any[] = [];
+  public tableContent: any[][] = [];
+
+  // image related control
+  public displayImage: boolean = false;
+
+  // markdown control
+  public displayMarkdown: boolean = false;
+
+  // json control
+  public displayJson: boolean = false;
+
+  // video
+  public displayMP4: boolean = false;
+
+  // audio
+  public displayMP3: boolean = false;
+
+  // plain text & octet stream related control
+  public displayPlainText: boolean = false;
+  public textContent: string = "";
+
+  // control flags
+  public isLoading: boolean = false;
+  public isFileSizeUnloadable = false;
+  public isFileLoadingError: boolean = false;
+  public isFileTypePreviewUnsupported: boolean = false;
+
+  public currentFile: File | undefined = undefined;
+  @Input()
+  isMaximized: boolean = false;
+
+  @Input()
+  did: number | undefined;
+
+  @Input()
+  dvid: number | undefined;
+
+  @Input()
+  filePath: string = "";
+
+  @Input()
+  fileSize?: number;
+
+  @Input()
+  isLogin: boolean = false;
+
+  @Output()
+  loadFile = new EventEmitter<{ file: string; prefix: string }>();
+
+  constructor(
+    private datasetService: DatasetService,
+    private sanitizer: DomSanitizer,
+    private notificationService: NotificationService
+  ) {}
+
+  ngOnInit(): void {
+    this.reloadFileContent();
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if ((changes.did && changes.dvid) || changes.filePath) {
+      this.reloadFileContent();
+    }
+  }
+
+  ngOnDestroy(): void {
+    if (this.fileURL) {
+      URL.revokeObjectURL(this.fileURL);
+    }
+  }
+
+  showImageModal = false;
+
+  toggleImageModal() {
+    this.showImageModal = !this.showImageModal;
+  }
+
+  reloadFileContent() {
+    this.turnOffAllDisplay();
+
+    // Pre-check - file size
+    const mimeType = getMimeType(this.filePath);
+    if (!this.isPreviewSupported(mimeType)) {
+      this.onFileTypePreviewUnsupported();
+      return;
+    }
+    const limit = MIME_TYPE_SIZE_LIMITS_MB[mimeType] ?? this.DEFAULT_MAX_SIZE;
+    if (this.fileSize != null && this.fileSize > limit) {
+      this.onFileSizeNotLoadable();
+      return;
+    }
+
+    // Load file
+    this.isLoading = true;
+    if (this.did && this.dvid && this.filePath != "") {
+      this.datasetService
+        .retrieveDatasetVersionSingleFile(this.filePath, this.isLogin)
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: blob => {
+            this.isLoading = false;
+            const blobMimeType = getMimeType(this.filePath);
+            if (!this.isPreviewSupported(blobMimeType)) {
+              this.onFileTypePreviewUnsupported();
+              return;
+            }
+            const MaxSize = MIME_TYPE_SIZE_LIMITS_MB[blobMimeType] || this.DEFAULT_MAX_SIZE;
+            const fileSize = blob.size;
+            if (fileSize > MaxSize) {
+              this.onFileSizeNotLoadable();
+              this.notificationService.warning(`File ${this.filePath} is too large to be previewed`);
+              return;
+            }
+            this.currentFile = new File([blob], this.filePath, { type: blob.type });
+            // Handle different file types
+            switch (blobMimeType) {
+              case MIME_TYPES.PNG:
+              case MIME_TYPES.JPEG:
+                this.displayImage = true;
+                this.loadSafeURL(blob);
+                break;
+              case MIME_TYPES.MP4:
+                this.displayMP4 = true;
+                this.loadSafeURL(blob);
+                break;
+
+              case MIME_TYPES.MP3:
+                this.displayMP3 = true;
+                this.loadSafeURL(blob);
+                break;
+
+              case MIME_TYPES.MSEXCEL:
+                readXlsxFile(blob).then(rows => {
+                  let parsedData: string[][] = [];
+                  rows.forEach(row => {
+                    // Convert each cell in the row to a string
+                    let stringRow = row.map(cell => (cell ? cell.toString() : ""));
+                    // Add the string array to the main array
+                    parsedData.push(stringRow);
+                  });
+                  if (parsedData.length > 0) {
+                    this.loadTabularFile(parsedData);
+                    this.displayXlsx = true;
+                  }
+                });
+                break;
+              case MIME_TYPES.CSV:
+                this.displayCSV = true;
+                // Handle CSV display
+                Papa.parse(this.currentFile, {
+                  complete: (results: ParseResult<any>) => {
+                    if (results.data.length > 0) {
+                      this.loadTabularFile(results.data);
+                    }
+                  },
+                  error: error => {
+                    console.error("Error parsing file:", error);
+                    this.onFileLoadingError();
+                  },
+                });
+                break;
+              case MIME_TYPES.MD:
+                this.displayMarkdown = true;
+                this.readFileAsText(blob);
+                break;
+              case MIME_TYPES.JSON:
+                this.displayJson = true;
+                this.readFileAsText(blob);
+                break;
+              case MIME_TYPES.TXT:
+              default:
+                this.displayPlainText = true;
+                this.readFileAsText(blob);
+                break;
+            }
+          },
+        });
+    }
+  }
+
+  turnOffAllDisplay() {
+    this.displayCSV = false;
+    this.displayXlsx = false;
+    this.displayImage = false;
+    this.displayPlainText = false;
+    this.displayMarkdown = false;
+    this.displayJson = false;
+    this.displayMP4 = false;
+    this.displayMP3 = false;
+    this.isLoading = false;
+    this.isFileLoadingError = false;
+    this.isFileSizeUnloadable = false;
+    this.isFileTypePreviewUnsupported = false;
+    // garbage collection
+    if (this.fileURL) {
+      URL.revokeObjectURL(this.fileURL);
+    }
+    if (this.safeFileURL) {
+      URL.revokeObjectURL(this.safeFileURL.toString());
+    }
+  }
+
+  onFileLoadingError() {
+    this.turnOffAllDisplay();
+    this.isFileLoadingError = true;
+  }
+
+  onFileSizeNotLoadable() {
+    this.turnOffAllDisplay();
+    this.isFileSizeUnloadable = true;
+  }
+
+  onFileTypePreviewUnsupported() {
+    this.turnOffAllDisplay();
+    this.isFileTypePreviewUnsupported = true;
+  }
+
+  isPreviewSupported(mimeType: string) {
+    return mimeType !== MIME_TYPES.OCTET_STREAM && Object.hasOwnProperty.call(MIME_TYPE_SIZE_LIMITS_MB, mimeType);
+  }
+
+  private readFileAsText(blob: Blob) {
+    const txtReader = new FileReader();
+    txtReader.onload = (event: any) => {
+      this.textContent = event.target.result;
+    };
+    txtReader.readAsText(blob);
+  }
+
+  private loadSafeURL(blob: Blob) {
+    this.fileURL = URL.createObjectURL(blob);
+    this.safeFileURL = this.sanitizer.bypassSecurityTrustUrl(this.fileURL);
+  }
+
+  private loadTabularFile(data: any[][]) {
+    if (data.length > 0) {
+      // Extract the header (first row)
+      this.tableDataHeader = data[0];
+
+      // Process the rest of the rows
+      this.tableContent = data
+        .slice(1)
+        .map(row => {
+          // Normalize the row length to match the header length
+          while (row.length < this.tableDataHeader.length) {
+            row.push("");
+          }
+          return row;
+        })
+        .filter(row => {
+          // filter out all empty row
+          let areCellAllEmpty = true;
+          for (const cell in row) {
+            if (cell != "") {
+              areCellAllEmpty = false;
+              break;
+            }
+          }
+          return !areCellAllEmpty;
+        });
+    }
+  }
+}
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.html b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.html
new file mode 100644
index 0000000000..5b1dece350
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.html
@@ -0,0 +1,58 @@
+<!--
+ 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.
+-->
+
+<div class="staged-object-list-container">
+  <nz-list
+    nzBordered
+    nzSize="small"
+    *ngIf="datasetStagedObjects.length > 0">
+    <nz-list-item *ngFor="let obj of datasetStagedObjects">
+      <span>
+        <nz-tag [nzColor]="obj.diffType === 'added' ? 'green' : 'red'"> {{ obj.diffType }} </nz-tag>
+      </span>
+      <span
+        class="truncate-file-path"
+        nz-tooltip
+        [nzTooltipTitle]="fileTooltipTpl">
+        {{ obj.path }}
+      </span>
+      <ng-template #fileTooltipTpl>
+        <div>{{ obj.path }}</div>
+        <div *ngIf="getFileUploadTime(obj.path) as uploadTime">Upload time: {{ formatTime(uploadTime) }}</div>
+      </ng-template>
+      <!-- Small delete button with tooltip -->
+      <button
+        nz-button
+        nzType="link"
+        class="delete-button"
+        nz-tooltip
+        [nzTooltipTitle]="'Revert the change'"
+        (click)="onObjectReverted(obj)">
+        <i
+          nz-icon
+          nzType="delete"
+          nzTheme="outline"></i>
+      </button>
+    </nz-list-item>
+  </nz-list>
+
+  <nz-empty
+    *ngIf="datasetStagedObjects.length === 0"
+    nzNotFoundContent="No pending changes"></nz-empty>
+</div>
diff --git a/core/gui/src/app/common/type/dataset.ts b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.scss
similarity index 53%
copy from core/gui/src/app/common/type/dataset.ts
copy to core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.scss
index 7825ca2797..165ea73b31 100644
--- a/core/gui/src/app/common/type/dataset.ts
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.scss
@@ -17,25 +17,27 @@
  * under the License.
  */
 
-import { DatasetFileNode } from "./datasetVersionFileTree";
+/* Styles for the file tree container */
+.staged-object-list-container {
+  max-height: 200px; /* Adjust the max-height as needed */
+  overflow-y: auto; /* Enables vertical scrolling when content exceeds max-height */
+  overflow-x: auto; /* Prevents horizontal scrolling */
+}
 
-export interface DatasetVersion {
-  dvid: number | undefined;
-  did: number;
-  creatorUid: number;
-  name: string;
-  versionHash: string | undefined;
-  creationTime: number | undefined;
-  fileNodes: DatasetFileNode[] | undefined;
+.truncate-file-path {
+  display: inline-block;
+  max-width: 250px; /* Adjust width as needed */
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
-export interface Dataset {
-  did: number | undefined;
-  ownerUid: number | undefined;
-  name: string;
-  isPublic: boolean;
-  isDownloadable: boolean;
-  storagePath: string | undefined;
-  description: string;
-  creationTime: number | undefined;
+.delete-button {
+  width: 24px; /* Minimum width for button */
+  height: 24px; /* Keep the button small */
+  padding: 0; /* Remove extra padding */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-width: unset; /* Prevents unnecessary expansion */
 }
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.ts b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.ts
new file mode 100644
index 0000000000..7856ff47ba
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-staged-objects-list/user-dataset-staged-objects-list.component.ts
@@ -0,0 +1,94 @@
+/**
+ * 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 { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
+import { DatasetStagedObject } from "../../../../../../common/type/dataset-staged-object";
+import { DatasetService } from "../../../../../service/user/dataset/dataset.service";
+import { NotificationService } from "../../../../../../common/service/notification/notification.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { formatTime } from "src/app/common/util/format.util";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-dataset-staged-objects-list",
+  templateUrl: "./user-dataset-staged-objects-list.component.html",
+  styleUrls: ["./user-dataset-staged-objects-list.component.scss"],
+})
+export class UserDatasetStagedObjectsListComponent implements OnInit {
+  @Input() did?: number; // Dataset ID
+  @Input() set userMakeChangesEvent(event: EventEmitter<void>) {
+    if (event) {
+      event.pipe(untilDestroyed(this)).subscribe(() => {
+        this.fetchDatasetStagedObjects();
+      });
+    }
+  }
+  @Input() uploadTimeMap?: Map<string, number>;
+
+  @Output() stagedObjectsChanged = new EventEmitter<DatasetStagedObject[]>(); // Emits staged objects list
+
+  datasetStagedObjects: DatasetStagedObject[] = [];
+  formatTime = formatTime;
+
+  constructor(
+    private datasetService: DatasetService,
+    private notificationService: NotificationService
+  ) {}
+
+  ngOnInit(): void {
+    this.fetchDatasetStagedObjects();
+  }
+
+  private fetchDatasetStagedObjects(): void {
+    if (this.did != undefined) {
+      this.datasetService
+        .getDatasetDiff(this.did)
+        .pipe(untilDestroyed(this))
+        .subscribe(diffs => {
+          this.datasetStagedObjects = diffs;
+          // Emit the updated staged objects list
+          this.stagedObjectsChanged.emit(this.datasetStagedObjects);
+        });
+    }
+  }
+
+  onObjectReverted(objDiff: DatasetStagedObject) {
+    if (this.did) {
+      this.datasetService
+        .resetDatasetFileDiff(this.did, objDiff.path)
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: (res: Response) => {
+            this.notificationService.success(`"${objDiff.diffType} ${objDiff.path}" is successfully reverted`);
+            this.fetchDatasetStagedObjects();
+          },
+          error: (err: unknown) => {
+            this.notificationService.error("Failed to delete the file");
+          },
+        });
+    }
+  }
+
+  getFileUploadTime(filePath: string): number | null {
+    if (!this.uploadTimeMap) return null;
+
+    const filename = filePath.split("/").pop() || filePath;
+    return this.uploadTimeMap.get(filename) || null;
+  }
+}
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component.html b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component.html
new file mode 100644
index 0000000000..2e4d86da61
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component.html
@@ -0,0 +1,72 @@
+<!--
+ 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.
+-->
+
+<div>
+  <nz-spin
+    [nzSpinning]="isCreating"
+    nzSize="large"
+    nzTip="Creating...">
+    <div
+      class="texera-user-dataset-creator-form"
+      [ngClass]="{'disabled-backdrop': isCreating}">
+      <formly-form
+        [form]="form"
+        [fields]="fields"
+        [model]="model"></formly-form>
+      <div
+        *ngIf="!isCreatingVersion"
+        class="dataset-toggles">
+        <div class="toggle-item">
+          <label>Visibility:</label>
+          <nz-switch
+            [ngModel]="isModelPublic"
+            (ngModelChange)="onPublicStatusChange($event)"
+            nzCheckedChildren="public"
+            nzUnCheckedChildren="private"></nz-switch>
+        </div>
+        <div class="toggle-item">
+          <label>Downloadable:</label>
+          <nz-switch
+            [ngModel]="isModelDownloadable"
+            (ngModelChange)="onDownloadableStatusChange($event)"
+            nzCheckedChildren="enabled"
+            nzUnCheckedChildren="disabled"></nz-switch>
+        </div>
+      </div>
+    </div>
+
+    <button
+      nz-button
+      nzType="primary"
+      (click)="onClickCreate()"
+      [disabled]="isCreateButtonDisabled || isCreating"
+      class="create-btn">
+      Create
+    </button>
+
+    <button
+      nz-button
+      nzType="default"
+      (click)="onClickCancel()"
+      class="cancel-btn"
+      [disabled]="isCreating">
+      Cancel
+    </button>
+  </nz-spin>
+</div>
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component.scss b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component.scss
new file mode 100644
index 0000000000..c6d9dda556
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component.scss
@@ -0,0 +1,86 @@
+/**
+ * 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.
+ */
+
+.texera-user-dataset-creator-form {
+  width: 80%;
+  margin: auto;
+}
+
+.create-btn {
+  background-color: #ffffff;
+  color: #007bff;
+  border: 1px solid lightgray;
+  padding: 5px 20px;
+  cursor: pointer;
+  margin-left: 15%;
+  margin-right: 5%;
+  width: 30%;
+  text-align: center;
+  &:hover {
+    background-color: darken(#007bff, 10%);
+    color: #ffffff;
+  }
+}
+
+.cancel-btn {
+  background-color: #ffffff;
+  color: #ff0000;
+  border: 1px solid lightgray;
+  padding: 5px 20px;
+  cursor: pointer;
+  margin-right: 15%;
+  margin-left: 5%;
+  margin-top: 10%;
+  width: 30%;
+  text-align: center;
+  &:hover {
+    background-color: darken(#ff0000, 10%);
+    color: #ffffff;
+  }
+}
+
+.file-uploader {
+  height: 40%;
+  width: 90%;
+  margin: auto;
+}
+
+.dataset-toggles {
+  margin-top: 16px;
+  margin-bottom: 16px;
+}
+
+.toggle-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+
+  label {
+    margin-right: 12px;
+    min-width: 80px;
+    font-weight: 500;
+  }
+
+  .help-text {
+    margin-left: 8px;
+    font-size: 12px;
+    color: #999;
+    font-style: italic;
+  }
+}
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component.ts b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component.ts
new file mode 100644
index 0000000000..9925a7a088
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component.ts
@@ -0,0 +1,220 @@
+/**
+ * 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 { Component, EventEmitter, inject, Input, OnInit, Output } from "@angular/core";
+import { FormBuilder, FormGroup, Validators } from "@angular/forms";
+import { FormlyFieldConfig } from "@ngx-formly/core";
+import { DatasetService } from "../../../../../service/user/dataset/dataset.service";
+import { Dataset, Model } from "../../../../../../common/type/dataset";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { NotificationService } from "../../../../../../common/service/notification/notification.service";
+import { HttpErrorResponse } from "@angular/common/http";
+import { NZ_MODAL_DATA, NzModalRef } from "ng-zorro-antd/modal";
+import { ModelService } from "../../../../../service/user/model/model.service";
+import { formatSpeed, formatTime } from "../../../../../../common/util/format.util";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-model-version-creator",
+  templateUrl: "./user-model-version-creator.component.html",
+  styleUrls: ["./user-model-version-creator.component.scss"],
+})
+export class UserModelVersionCreatorComponent implements OnInit {
+  readonly isCreatingVersion: boolean = inject(NZ_MODAL_DATA).isCreatingVersion;
+
+  readonly mid: number = inject(NZ_MODAL_DATA)?.mid ?? undefined;
+
+  isCreateButtonDisabled: boolean = false;
+
+  public form: FormGroup = new FormGroup({});
+  model: any = {};
+  fields: FormlyFieldConfig[] = [];
+  isModelPublic: boolean = false;
+  isModelDownloadable: boolean = false;
+
+  // used when creating the dataset
+  isDatasetNameSanitized: boolean = false;
+
+  // boolean to control if is uploading
+  isCreating: boolean = false;
+
+  constructor(
+    private modalRef: NzModalRef,
+    private modelService: ModelService,
+    private notificationService: NotificationService,
+    private formBuilder: FormBuilder
+  ) {}
+
+  ngOnInit() {
+    this.setFormFields();
+    this.isDatasetNameSanitized = false;
+  }
+
+  private setFormFields() {
+    this.fields = this.isCreatingVersion
+      ? [
+          // Fields when isCreatingVersion is true
+          {
+            key: "versionDescription",
+            type: "input",
+            defaultValue: "",
+            templateOptions: {
+              label: "Describe the new version",
+              required: false,
+            },
+          },
+        ]
+      : [
+          // Fields when isCreatingVersion is false
+          {
+            key: "name",
+            type: "input",
+            templateOptions: {
+              label: "Name",
+              required: true,
+            },
+          },
+          {
+            key: "description",
+            type: "input",
+            defaultValue: "",
+            templateOptions: {
+              label: "Description",
+            },
+          },
+        ];
+  }
+  get formControlNames(): string[] {
+    return Object.keys(this.form.controls);
+  }
+
+  datasetNameSanitization(datasetName: string): string {
+    // Remove leading spaces
+    let sanitizedDatasetName = datasetName.trimStart();
+
+    // Replace all characters that are not letters (a-z, A-Z), numbers (0-9) with a short dash "-"
+    sanitizedDatasetName = sanitizedDatasetName.replace(/[^a-zA-Z0-9]+/g, "-");
+
+    // Lower-case everything
+    sanitizedDatasetName = sanitizedDatasetName.toLowerCase();
+
+    // Track whether user’s input be changed
+    if (sanitizedDatasetName !== datasetName) {
+      this.isDatasetNameSanitized = true;
+    }
+
+    return sanitizedDatasetName;
+  }
+
+  private triggerValidation() {
+    Object.keys(this.form.controls).forEach(field => {
+      const control = this.form.get(field);
+      control?.markAsTouched({ onlySelf: true });
+    });
+  }
+
+  onClickCancel() {
+    this.modalRef.close(null);
+  }
+
+  onClickCreate() {
+    // check if the form is valid
+    this.triggerValidation();
+
+    if (!this.form.valid) {
+      return; // Stop further execution if the form is not valid
+    }
+
+    this.isCreating = true;
+    if (this.isCreatingVersion && this.mid) {
+      const versionName = this.form.get("versionDescription")?.value;
+      this.modelService
+        .createDatasetVersion(this.mid, versionName)
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: res => {
+            this.notificationService.success("Version Created");
+            this.isCreating = false;
+            // creation succeed, emit created version
+            this.modalRef.close(res);
+          },
+          error: (res: unknown) => {
+            const err = res as HttpErrorResponse;
+            this.notificationService.error(`Version creation failed: ${err.error.message}`);
+            this.isCreating = false;
+            // creation failed, emit null value
+            this.modalRef.close(null);
+          },
+        });
+    } else {
+      // capture original and sanitized names
+      const originalName = this.form.get("name")?.value as string;
+      const sanitizedName = this.datasetNameSanitization(originalName);
+
+      const model: Model = {
+        name: sanitizedName,
+        description: this.form.get("description")?.value,
+        isPublic: this.isModelPublic,
+        isDownloadable: this.isModelDownloadable,
+        mid: undefined,
+        ownerUid: undefined,
+        storagePath: undefined,
+        creationTime: undefined,
+      };
+      this.modelService
+        .createModel(model)
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: res => {
+            const msg = this.isDatasetNameSanitized
+              ? `Dataset '${originalName}' was sanitized to '${sanitizedName}' and created successfully.`
+              : `Dataset '${sanitizedName}' created successfully.`;
+
+            this.notificationService.success(msg);
+            this.isCreating = false;
+            // if creation succeed, emit the created dashboard dataset
+            this.modalRef.close(res);
+          },
+          error: (res: unknown) => {
+            const err = res as HttpErrorResponse;
+            this.notificationService.error(`Model ${model.name} creation failed: ${err.error.message}`);
+            this.isCreating = false;
+            // if creation failed, emit null value
+            this.modalRef.close(null);
+          },
+        });
+    }
+  }
+
+  onPublicStatusChange(newValue: boolean): void {
+    // Handle the change in dataset public status
+    this.isModelPublic = newValue;
+  }
+
+  onDownloadableStatusChange(newValue: boolean): void {
+    // Handle the change in dataset downloadable status
+    this.isModelDownloadable = newValue;
+  }
+
+  protected readonly formatSpeed = formatSpeed;
+  protected readonly formatTime = formatTime;
+
+
+
+}
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.html b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.html
new file mode 100644
index 0000000000..f09cdb37d4
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.html
@@ -0,0 +1,54 @@
+<!--
+ 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.
+-->
+
+<div class="file-tree-container">
+  <tree-root
+    #tree
+    [nodes]="fileTreeNodes"
+    [options]="fileTreeDisplayOptions">
+    <ng-template
+      #treeNodeTemplate
+      let-node>
+      <span title="{{ node.data.name }}">
+        <i
+          *ngIf="node.data.children"
+          nz-icon
+          nzType="folder"
+          nzTheme="outline"></i>
+        <i
+          *ngIf="!node.data.children"
+          nz-icon
+          nzType="file"
+          nzTheme="outline"></i>
+        {{ node.data.name }}
+        <button
+          nz-button
+          nzType="link"
+          *ngIf="isTreeNodeDeletable && !node.data.children"
+          class="delete-button"
+          (click)="onNodeDeleted(node.data)">
+          <i
+            nz-icon
+            nzType="delete"
+            nzTheme="outline"></i>
+        </button>
+      </span>
+    </ng-template>
+  </tree-root>
+</div>
diff --git a/core/gui/src/app/common/type/dataset.ts b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.scss
similarity index 55%
copy from core/gui/src/app/common/type/dataset.ts
copy to core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.scss
index 7825ca2797..edfaab6128 100644
--- a/core/gui/src/app/common/type/dataset.ts
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.scss
@@ -17,25 +17,31 @@
  * under the License.
  */
 
-import { DatasetFileNode } from "./datasetVersionFileTree";
+.file-display-padding {
+  width: 20px;
+}
 
-export interface DatasetVersion {
-  dvid: number | undefined;
-  did: number;
-  creatorUid: number;
-  name: string;
-  versionHash: string | undefined;
-  creationTime: number | undefined;
-  fileNodes: DatasetFileNode[] | undefined;
+/* Styles for the delete button */
+.delete-button {
+  width: 15px;
+  margin-left: 5px;
 }
 
-export interface Dataset {
-  did: number | undefined;
-  ownerUid: number | undefined;
-  name: string;
-  isPublic: boolean;
-  isDownloadable: boolean;
-  storagePath: string | undefined;
-  description: string;
-  creationTime: number | undefined;
+/* Styles for the file tree container */
+.file-tree-container {
+  max-height: 200px; /* Adjust the max-height as needed */
+  overflow-y: auto; /* Enables vertical scrolling when content exceeds max-height */
+  overflow-x: auto; /* Prevents horizontal scrolling */
 }
+
+//tree-root .fa-file {
+//  //padding-left: 4%; /* Adjust the value as needed */
+//}
+//
+//tree-root span {
+//  display: inline-block;
+//  max-width: 100%; /* Adjust the width as needed */
+//  text-overflow: ellipsis;
+//  white-space: nowrap;
+//  overflow: hidden;
+//}
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts
new file mode 100644
index 0000000000..94e8c98c88
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.ts
@@ -0,0 +1,77 @@
+/**
+ * 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 { UntilDestroy } from "@ngneat/until-destroy";
+import { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild } from "@angular/core";
+import { DatasetFileNode, getFullPathFromDatasetFileNode } from "../../../../../../common/type/datasetVersionFileTree";
+import { ITreeOptions, TREE_ACTIONS } from "@ali-hm/angular-tree-component";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-dataset-version-filetree",
+  templateUrl: "./user-dataset-version-filetree.component.html",
+  styleUrls: ["./user-dataset-version-filetree.component.scss"],
+})
+export class UserDatasetVersionFiletreeComponent implements AfterViewInit {
+  @Input()
+  public isTreeNodeDeletable: boolean = false;
+
+  @Input()
+  public fileTreeNodes: DatasetFileNode[] = [];
+
+  @Input()
+  public isExpandAllAfterViewInit = false;
+
+  @ViewChild("tree") tree: any;
+
+  public fileTreeDisplayOptions: ITreeOptions = {
+    displayField: "name",
+    hasChildrenField: "children",
+    actionMapping: {
+      mouse: {
+        click: (tree: any, node: any, $event: any) => {
+          if (node.hasChildren) {
+            TREE_ACTIONS.TOGGLE_EXPANDED(tree, node, $event);
+          } else {
+            this.selectedTreeNode.emit(node.data);
+          }
+        },
+      },
+    },
+  };
+
+  @Output()
+  public selectedTreeNode = new EventEmitter<DatasetFileNode>();
+
+  @Output()
+  public deletedTreeNode = new EventEmitter<DatasetFileNode>();
+
+  constructor() {}
+
+  onNodeDeleted(node: DatasetFileNode): void {
+    // look up for the DatasetVersionFileTreeNode
+    this.deletedTreeNode.emit(node);
+  }
+
+  ngAfterViewInit(): void {
+    if (this.isExpandAllAfterViewInit) {
+      this.tree.treeModel.expandAll();
+    }
+  }
+}
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-list-item/user-dataset-list-item.component.html b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-list-item/user-dataset-list-item.component.html
new file mode 100644
index 0000000000..4f7d5e0195
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-list-item/user-dataset-list-item.component.html
@@ -0,0 +1,154 @@
+<!--
+ 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.
+-->
+
+<nz-list-item class="dataset-list-item">
+  <nz-list-item-meta class="dataset-item-meta">
+    <!-- Margin need only when the checkbox is not visible -->
+    <nz-list-item-meta-avatar [ngStyle]="{ 'margin-left': editable ? 'none' : '16px' }">
+      <nz-avatar
+        [ngStyle]="{ 'background-color': 'grey', 'vertical-align': 'middle' }"
+        [nzGap]="4"
+        [nzText]="'' + dataset.did"
+        nzSize="default"></nz-avatar>
+    </nz-list-item-meta-avatar>
+
+    <!-- editable name of saved dataset -->
+    <nz-list-item-meta-title class="meta-title-container">
+      <div class="dataset-item-meta-title">
+        <a
+          *ngIf="!editingName; else customDatasetTitle "
+          [routerLink]="DASHBOARD_USER_DATASET + '/' + dataset.did"
+          class="dataset-name"
+          >{{ dataset.name }}</a
+        >
+        <ng-template #customDatasetTitle>
+          <input
+            #customName
+            (focusout)="confirmUpdateDatasetCustomName(customName.value)"
+            (keyup.enter)="confirmUpdateDatasetCustomName(customName.value)"
+            placeholder="{{ dataset.name }}"
+            value="{{ dataset.name }}" />
+        </ng-template>
+        <button
+          *ngIf="editable && entry.accessPrivilege === 'WRITE'"
+          (click)="editingName = true"
+          nz-button
+          mat-card-title="Customize Dataset Name"
+          nz-tooltip="Customize Dataset Name"
+          nzSize="small"
+          nzTooltipPlacement="bottom"
+          nzType="text">
+          <i
+            nz-icon
+            nzTheme="outline"
+            nzType="edit"></i>
+        </button>
+        <button
+          *ngIf="editable && entry.accessPrivilege === 'WRITE'"
+          (click)="editingDescription = true"
+          nz-button
+          nz-tooltip="Add Description"
+          nzSize="small"
+          nzTooltipPlacement="bottom"
+          nzType="text"
+          class="add-description-btn">
+          <i
+            nz-icon
+            nzTheme="outline"
+            nzType="plus-square"></i>
+        </button>
+        <i
+          class="dataset-is-owner-icon"
+          *ngIf="entry.isOwner"
+          nz-tooltip="You are the owner"
+          nzTooltipPlacement="bottom"
+          nz-icon
+          nzTheme="outline"
+          nzType="user"></i>
+        <i
+          *ngIf="!entry.isOwner"
+          nz-tooltip="{{
+                                  entry.accessPrivilege
+                              }} Access"
+          nzTooltipPlacement="bottom"
+          nz-icon
+          nzTheme="outline"
+          nzType="team"></i>
+      </div>
+    </nz-list-item-meta-title>
+
+    <!-- editable description of saved dataset -->
+    <nz-list-item-meta-description>
+      <div class="dataset-item-meta-description">
+        <label
+          *ngIf="!editingDescription; else customDatasetDescription "
+          (click)="editingDescription = editable && entry.accessPrivilege == 'WRITE'"
+          class="dataset-description-label">
+          {{ dataset.description }}
+        </label>
+        <ng-template #customDatasetDescription>
+          <input
+            title="Description"
+            #customDescription
+            (focusout)="confirmUpdateDatasetCustomDescription(customDescription.value)"
+            (keyup.enter)="confirmUpdateDatasetCustomDescription(customDescription.value)"
+            class="dataset-editable-description-input"
+            value="{{ dataset.description }}"
+            maxlength="500" />
+        </ng-template>
+      </div>
+    </nz-list-item-meta-description>
+  </nz-list-item-meta>
+
+  <ul nz-list-item-actions>
+    <nz-list-item-action>
+      <button
+        (click)="onClickOpenShareAccess()"
+        nz-button
+        nz-tooltip="Share the dataset {{
+                      dataset.name
+                  }} to others"
+        nzTooltipPlacement="bottom"
+        type="button">
+        <i
+          nz-icon
+          nzTheme="outline"
+          nzType="share-alt"></i>
+      </button>
+    </nz-list-item-action>
+    <nz-list-item-action *ngIf="editable">
+      <button
+        (nzOnConfirm)="deleted.emit()"
+        nz-popconfirm
+        nzPopconfirmTitle="Confirm to delete this dataset."
+        [disabled]="!entry.isOwner"
+        class="delete-dataset-btn"
+        nz-button
+        nz-tooltip="Delete the dataset {{
+                      dataset.name
+                  }}"
+        nzTooltipPlacement="bottom">
+        <i
+          nz-icon
+          nzTheme="outline"
+          nzType="delete"></i>
+      </button>
+    </nz-list-item-action>
+  </ul>
+</nz-list-item>
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-list-item/user-dataset-list-item.component.scss b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-list-item/user-dataset-list-item.component.scss
new file mode 100644
index 0000000000..e06ff48c19
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-list-item/user-dataset-list-item.component.scss
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+@import "../../../section-style";
+@import "../../../dashboard.component.scss";
+
+/**
+  * css for project label, shared by workflow section and file section
+**/
+.dataset-list-item {
+  margin-bottom: 10px;
+  min-height: 70px;
+  padding: 5px 0 5px 0;
+
+  .dataset-item-meta {
+    margin: 0 20px;
+  }
+
+  .dataset-item-meta-title {
+    display: flex;
+    align-items: center;
+
+    .dataset-name {
+      color: black;
+      font-size: 20px;
+      font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+      text-align: center;
+    }
+
+    i {
+      position: relative;
+      font-size: 17px;
+      margin-bottom: 6px;
+    }
+
+    i.dataset-is-owner-icon {
+      margin-left: 7px;
+    }
+  }
+
+  .dataset-item-meta-description {
+    display: flex;
+    align-items: center;
+    padding: 2px 8px 2px 10px;
+    margin-bottom: 8px;
+
+    .dataset-description-label {
+      font-size: 13px;
+      font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+
+      display: inline-block;
+      min-width: 17ch;
+      border: 0 none;
+      outline: none;
+
+      &:hover {
+        cursor: pointer;
+        box-shadow: 0 0 0 1px rgb(202, 202, 202);
+      }
+    }
+
+    .dataset-editable-description-input {
+      margin-bottom: 5px;
+      display: inline-block;
+      min-width: 17ch;
+      border: 0 none;
+      outline: none;
+      box-shadow: 0 0 0 2px #007bff;
+    }
+  }
+}
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-dataset-list-item/user-dataset-list-item.component.ts b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-list-item/user-dataset-list-item.component.ts
new file mode 100644
index 0000000000..c18d37ee0c
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-dataset-list-item/user-dataset-list-item.component.ts
@@ -0,0 +1,133 @@
+/**
+ * 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 { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { Component, EventEmitter, Input, Output } from "@angular/core";
+import { Dataset } from "../../../../../common/type/dataset";
+import { DatasetService } from "../../../../service/user/dataset/dataset.service";
+import { ShareAccessComponent } from "../../share-access/share-access.component";
+import { NotificationService } from "../../../../../common/service/notification/notification.service";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { DashboardDataset } from "../../../../type/dashboard-dataset.interface";
+import { DASHBOARD_USER_DATASET } from "../../../../../app-routing.constant";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-dataset-list-item",
+  templateUrl: "./user-dataset-list-item.component.html",
+  styleUrls: ["./user-dataset-list-item.component.scss"],
+})
+export class UserDatasetListItemComponent {
+  protected readonly DASHBOARD_USER_DATASET = DASHBOARD_USER_DATASET;
+
+  private _entry?: DashboardDataset;
+
+  @Output()
+  refresh = new EventEmitter<void>();
+
+  @Input()
+  get entry(): DashboardDataset {
+    if (!this._entry) {
+      throw new Error("entry property must be provided to UserDatasetListItemComponent.");
+    }
+    return this._entry;
+  }
+
+  set entry(value: DashboardDataset) {
+    this._entry = value;
+  }
+
+  get dataset(): Dataset {
+    if (!this.entry.dataset) {
+      throw new Error(
+        "Incorrect type of DashboardEntry provided to UserDatasetListItemComponent. Entry must be dataset."
+      );
+    }
+    return this.entry.dataset;
+  }
+
+  @Input() editable = false;
+  @Output() deleted = new EventEmitter<void>();
+  @Output() duplicated = new EventEmitter<void>();
+
+  editingName = false;
+  editingDescription = false;
+
+  constructor(
+    private modalService: NzModalService,
+    private datasetService: DatasetService,
+    private notificationService: NotificationService
+  ) {}
+
+  public confirmUpdateDatasetCustomName(name: string) {
+    if (this.entry.dataset.name === name) {
+      return;
+    }
+
+    if (this.entry.dataset.did)
+      this.datasetService
+        .updateDatasetName(this.entry.dataset.did, name)
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: () => {
+            this.entry.dataset.name = name;
+            this.editingName = false;
+          },
+          error: () => {
+            this.notificationService.error("Update dataset name failed");
+            this.editingName = false;
+          },
+        });
+  }
+
+  public confirmUpdateDatasetCustomDescription(description: string) {
+    if (this.entry.dataset.description === description) {
+      return;
+    }
+
+    if (this.entry.dataset.did)
+      this.datasetService
+        .updateDatasetDescription(this.entry.dataset.did, description)
+        .pipe(untilDestroyed(this))
+        .subscribe({
+          next: () => {
+            this.entry.dataset.description = description;
+            this.editingDescription = false;
+          },
+          error: () => {
+            this.notificationService.error("Update dataset description failed");
+            this.editingDescription = false;
+          },
+        });
+  }
+
+  public onClickOpenShareAccess() {
+    this.modalService.create({
+      nzContent: ShareAccessComponent,
+      nzData: {
+        writeAccess: this.entry.accessPrivilege === "WRITE",
+        type: "dataset",
+        id: this.dataset.did,
+      },
+      nzFooter: null,
+      nzTitle: "Share this dataset with others",
+      nzCentered: true,
+    });
+  }
+}
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-model.component.html b/core/gui/src/app/dashboard/component/user/user-model/user-model.component.html
new file mode 100644
index 0000000000..ca89b08ef9
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-model.component.html
@@ -0,0 +1,61 @@
+<!--
+ 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.
+-->
+
+<div class="section-container subsection-grid-container">
+  <nz-card class="section-title">
+    <h2 class="page-title">Models</h2>
+    <div class="button-group">
+      <button
+        nz-button
+        class="create-btn"
+        (click)="onClickOpenModelAddComponent()"
+        title="Create Model">
+        <i
+          nz-icon
+          nzType="file-add"
+          nzTheme="outline"></i>
+        <span>Create Model</span>
+      </button>
+      <texera-filters #filters></texera-filters>
+    </div>
+  </nz-card>
+
+  <div class="section-search-bar">
+    <texera-filters-instructions></texera-filters-instructions>
+    <nz-select
+      #searchInput
+      class="search-input-box"
+      name="search-input-box"
+      nzMode="tags"
+      nzPlaceHolder="Search all models"
+      [nzBorderless]="true"
+      [nzOpen]="false"
+      ngDefaultControl
+      [(ngModel)]="filters.masterFilterList"
+      [nzAllowClear]="true">
+    </nz-select>
+  </div>
+
+  <texera-search-results
+    [editable]="true"
+    [isPrivateSearch]="true"
+    (deleted)="deleteModel($event)"
+    (refresh)="ngAfterViewInit()"
+    [currentUid]="currentUid"></texera-search-results>
+</div>
diff --git a/core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts b/core/gui/src/app/dashboard/component/user/user-model/user-model.component.scss
similarity index 72%
copy from core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts
copy to core/gui/src/app/dashboard/component/user/user-model/user-model.component.scss
index 2ea0058264..31c95188c4 100644
--- a/core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-model.component.scss
@@ -17,13 +17,11 @@
  * under the License.
  */
 
-import { Dataset, DatasetVersion } from "../../common/type/dataset";
-import { DatasetFileNode } from "../../common/type/datasetVersionFileTree";
+@import "../../dashboard.component";
+@import "../../section-style";
+@import "../../button-style";
 
-export interface DashboardDataset {
-  isOwner: boolean;
-  ownerEmail: string;
-  dataset: Dataset;
-  accessPrivilege: "READ" | "WRITE" | "NONE";
-  size: number;
+.dataset-menu-bar {
+  height: 55px;
+  overflow-y: hidden;
 }
diff --git a/core/gui/src/app/dashboard/component/user/user-model/user-model.component.ts b/core/gui/src/app/dashboard/component/user/user-model/user-model.component.ts
new file mode 100644
index 0000000000..b3a2a1bd9b
--- /dev/null
+++ b/core/gui/src/app/dashboard/component/user/user-model/user-model.component.ts
@@ -0,0 +1,207 @@
+/**
+ * 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 { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { AfterViewInit, Component, ViewChild } from "@angular/core";
+import { UserService } from "../../../../common/service/user/user.service";
+import { Router } from "@angular/router";
+import { SearchService } from "../../../service/user/search.service";
+import { DatasetService } from "../../../service/user/dataset/dataset.service";
+import { SortMethod } from "../../../type/sort-method";
+import { DashboardEntry, UserInfo } from "../../../type/dashboard-entry";
+import { SearchResultsComponent } from "../search-results/search-results.component";
+import { FiltersComponent } from "../filters/filters.component";
+import { firstValueFrom } from "rxjs";
+import {
+  DASHBOARD_USER_DATASET,
+  DASHBOARD_USER_DATASET_CREATE,
+  DASHBOARD_USER_MODEL,
+} from "../../../../app-routing.constant";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { FileSelectionComponent } from "../../../../workspace/component/file-selection/file-selection.component";
+import { DatasetFileNode, getFullPathFromDatasetFileNode } from "../../../../common/type/datasetVersionFileTree";
+import { UserModelVersionCreatorComponent } from "./user-dataset-explorer/user-dataset-version-creator/user-model-version-creator.component";
+import { DashboardModel } from "../../../type/dashboard-dataset.interface";
+import { NzMessageService } from "ng-zorro-antd/message";
+import { map, tap } from "rxjs/operators";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-model-section",
+  templateUrl: "user-model.component.html",
+  styleUrls: ["user-model.component.scss"],
+})
+export class UserModelComponent implements AfterViewInit {
+  public sortMethod = SortMethod.EditTimeDesc;
+  lastSortMethod: SortMethod | null = null;
+  public isLogin = this.userService.isLogin();
+  public currentUid = this.userService.getCurrentUser()?.uid;
+  public hasMismatch = false; // Display warning when there are mismatched datasets
+
+  private _searchResultsComponent?: SearchResultsComponent;
+  @ViewChild(SearchResultsComponent) get searchResultsComponent(): SearchResultsComponent {
+    if (this._searchResultsComponent) {
+      return this._searchResultsComponent;
+    }
+    throw new Error("Property cannot be accessed before it is initialized.");
+  }
+
+  set searchResultsComponent(value: SearchResultsComponent) {
+    this._searchResultsComponent = value;
+  }
+
+  private _filters?: FiltersComponent;
+  @ViewChild(FiltersComponent) get filters(): FiltersComponent {
+    if (this._filters) {
+      return this._filters;
+    }
+    throw new Error("Property cannot be accessed before it is initialized.");
+  }
+
+  set filters(value: FiltersComponent) {
+    value.masterFilterListChange.pipe(untilDestroyed(this)).subscribe({ next: () => this.search() });
+    this._filters = value;
+  }
+
+  private masterFilterList: ReadonlyArray<string> | null = null;
+  constructor(
+    private modalService: NzModalService,
+    private userService: UserService,
+    private router: Router,
+    private searchService: SearchService,
+    private datasetService: DatasetService,
+    private message: NzMessageService
+  ) {
+    this.userService
+      .userChanged()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        this.isLogin = this.userService.isLogin();
+        this.currentUid = this.userService.getCurrentUser()?.uid;
+      });
+  }
+
+  ngAfterViewInit() {
+    this.userService
+      .userChanged()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => this.search());
+  }
+
+  /*
+   * Executes a dataset search with filtering, sorting.
+   *
+   * Parameters:
+   * - filterScope = "all" | "public" | "private" - Determines visibility scope for search:
+   *  - "all": includes all datasets, public and private
+   *  - "public": limits the search to public datasets
+   *  - "private": limits the search to dataset where the user has direct access rights.
+   */
+  async search(forced: Boolean = false, filterScope: "all" | "public" | "private" = "private"): Promise<void> {
+    const sameList =
+      this.masterFilterList !== null &&
+      this.filters.masterFilterList.length === this.masterFilterList.length &&
+      this.filters.masterFilterList.every((v, i) => v === this.masterFilterList![i]);
+    if (!forced && sameList && this.sortMethod === this.lastSortMethod) {
+      // If the filter lists are the same, do no make the same request again.
+      return;
+    }
+    this.lastSortMethod = this.sortMethod;
+    this.masterFilterList = this.filters.masterFilterList;
+    if (!this.searchResultsComponent) {
+      throw new Error("searchResultsComponent is undefined.");
+    }
+    let filterParams = this.filters.getSearchFilterParameters();
+
+    // if the filter requires only public datasets, the public search should be invoked, and the search method should
+    // set the isLogin parameter to false in this case
+    const isLogin = filterScope === "public" ? false : this.isLogin;
+    const includePublic = filterScope === "all" || filterScope === "public";
+
+    this.searchResultsComponent.reset((start, count) => {
+      return firstValueFrom(
+        this.searchService
+          .executeSearch(
+            this.filters.getSearchKeywords(),
+            filterParams,
+            start,
+            count,
+            "model",
+            this.sortMethod,
+            isLogin,
+            includePublic
+          )
+          .pipe(
+            tap(({ hasMismatch }) => {
+              this.hasMismatch = hasMismatch ?? false;
+              if (this.hasMismatch) {
+                this.message.warning(
+                  "There is a mismatch between some models in the database and LakeFS. Only matched models are displayed.",
+                  { nzDuration: 4000 }
+                );
+              }
+            }),
+            map(({ entries, more }) => ({ entries, more }))
+          )
+      );
+    });
+    await this.searchResultsComponent.loadMore();
+  }
+
+  public onClickOpenModelAddComponent(): void {
+    const modal = this.modalService.create({
+      nzTitle: "Create New Model",
+      nzContent: UserModelVersionCreatorComponent,
+      nzFooter: null,
+      nzData: {
+        isCreatingVersion: false,
+      },
+      nzBodyStyle: {
+        resize: "both",
+        overflow: "auto",
+        minHeight: "200px",
+        minWidth: "550px",
+        maxWidth: "90vw",
+        maxHeight: "80vh",
+      },
+      nzWidth: "fit-content",
+    });
+    // Handle the selection from the modal
+    modal.afterClose.pipe(untilDestroyed(this)).subscribe(result => {
+      if (result != null) {
+        const dashboardModel: DashboardModel = result as DashboardModel;
+        this.router.navigate([`${DASHBOARD_USER_MODEL}/${dashboardModel.model.mid}`]);
+      }
+    });
+  }
+
+  public deleteModel(entry: DashboardEntry): void {
+    if (entry.model.model.mid == undefined) {
+      return;
+    }
+    this.datasetService
+      .deleteDatasets(entry.model.model.mid)
+      .pipe(untilDestroyed(this))
+      .subscribe(_ => {
+        this.searchResultsComponent.entries = this.searchResultsComponent.entries.filter(
+          modelEntry => modelEntry.model.model.mid !== entry.model.model.mid
+        );
+      });
+  }
+}
diff --git a/core/gui/src/app/dashboard/service/user/download/download.service.spec.ts b/core/gui/src/app/dashboard/service/user/download/download.service.spec.ts
index 3f59997078..156a125e5a 100644
--- a/core/gui/src/app/dashboard/service/user/download/download.service.spec.ts
+++ b/core/gui/src/app/dashboard/service/user/download/download.service.spec.ts
@@ -34,7 +34,7 @@ describe("DownloadService", () => {
   let notificationServiceSpy: jasmine.SpyObj<NotificationService>;
 
   beforeEach(() => {
-    const datasetSpy = jasmine.createSpyObj("DatasetService", [
+    const datasetSpy = jasmine.createSpyObj("ModelService", [
       "retrieveDatasetVersionSingleFile",
       "retrieveDatasetVersionZip", // Add this method to the spy
     ]);
diff --git a/core/gui/src/app/dashboard/service/user/model/model.service.ts b/core/gui/src/app/dashboard/service/user/model/model.service.ts
new file mode 100644
index 0000000000..d0f963013f
--- /dev/null
+++ b/core/gui/src/app/dashboard/service/user/model/model.service.ts
@@ -0,0 +1,524 @@
+/**
+ * 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 { Injectable } from "@angular/core";
+import { HttpClient, HttpParams, HttpResponse } from "@angular/common/http";
+import { catchError, map, mergeMap, switchMap, tap, toArray } from "rxjs/operators";
+import { Dataset, DatasetVersion, Model } from "../../../../common/type/dataset";
+import { AppSettings } from "../../../../common/app-setting";
+import { EMPTY, forkJoin, from, Observable, of, throwError } from "rxjs";
+import { DashboardDataset } from "../../../type/dashboard-dataset.interface";
+import { DatasetFileNode } from "../../../../common/type/datasetVersionFileTree";
+import { DatasetStagedObject } from "../../../../common/type/dataset-staged-object";
+import { GuiConfigService } from "../../../../common/service/gui-config.service";
+
+export const MODEL_BASE_URL = "model";
+export const MODEL_CREATE_URL = MODEL_BASE_URL + "/create";
+export const DATASET_UPDATE_BASE_URL = MODEL_BASE_URL + "/update";
+export const DATASET_UPDATE_NAME_URL = DATASET_UPDATE_BASE_URL + "/name";
+export const DATASET_UPDATE_DESCRIPTION_URL = DATASET_UPDATE_BASE_URL + "/description";
+export const DATASET_UPDATE_PUBLICITY_URL = "update/publicity";
+export const DATASET_UPDATE_DOWNLOADABLE_URL = "update/downloadable";
+export const DATASET_LIST_URL = MODEL_BASE_URL + "/list";
+export const DATASET_SEARCH_URL = MODEL_BASE_URL + "/search";
+export const DATASET_DELETE_URL = MODEL_BASE_URL + "/delete";
+
+export const DATASET_VERSION_BASE_URL = "version";
+export const DATASET_VERSION_RETRIEVE_LIST_URL = DATASET_VERSION_BASE_URL + "/list";
+export const DATASET_VERSION_LATEST_URL = DATASET_VERSION_BASE_URL + "/latest";
+export const DEFAULT_DATASET_NAME = "Untitled dataset";
+export const DATASET_PUBLIC_VERSION_BASE_URL = "publicVersion";
+export const DATASET_PUBLIC_VERSION_RETRIEVE_LIST_URL = DATASET_PUBLIC_VERSION_BASE_URL + "/list";
+export const DATASET_GET_OWNERS_URL = MODEL_BASE_URL + "/user-dataset-owners";
+
+export interface MultipartUploadProgress {
+  filePath: string;
+  percentage: number;
+  status: "initializing" | "uploading" | "finished" | "aborted";
+  uploadId: string;
+  physicalAddress: string;
+  uploadSpeed?: number; // bytes per second
+  estimatedTimeRemaining?: number; // seconds
+  totalTime?: number; // total seconds taken
+}
+
+@Injectable({
+  providedIn: "root",
+})
+export class ModelService {
+  constructor(
+    private http: HttpClient,
+    private config: GuiConfigService
+  ) {}
+
+  public createModel(model: Model): Observable<DashboardDataset> {
+    return this.http.post<DashboardDataset>(`${AppSettings.getApiEndpoint()}/${MODEL_CREATE_URL}`, {
+      modelName: model.name,
+      modelDescription: model.description,
+      isModelPublic: model.isPublic,
+      isModelDownloadable: model.isDownloadable,
+    });
+  }
+
+  public getDataset(did: number, isLogin: boolean = true): Observable<DashboardDataset> {
+    const apiUrl = isLogin
+      ? `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}`
+      : `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/public/${did}`;
+    return this.http.get<DashboardDataset>(apiUrl);
+  }
+
+  /**
+   * Retrieves a single file from a dataset version using a pre-signed URL.
+   * @param filePath Relative file path within the dataset.
+   * @param isLogin Determine whether a user is currently logged in
+   * @returns Observable<Blob>
+   */
+  public retrieveDatasetVersionSingleFile(filePath: string, isLogin: boolean = true): Observable<Blob> {
+    const endpointSegment = isLogin ? "presign-download" : "public-presign-download";
+    const endpoint = `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${endpointSegment}?filePath=${encodeURIComponent(filePath)}`;
+
+    return this.http
+      .get<{ presignedUrl: string }>(endpoint)
+      .pipe(switchMap(({ presignedUrl }) => this.http.get(presignedUrl, { responseType: "blob" })));
+  }
+
+  /**
+   * Retrieves a zip file of a dataset version.
+   * @param did Dataset ID
+   * @param dvid (Optional) Dataset version ID. If omitted, the latest version is downloaded.
+   * @returns An Observable that emits a Blob containing the zip file.
+   */
+  public retrieveDatasetVersionZip(did: number, dvid?: number): Observable<Blob> {
+    let params = new HttpParams();
+
+    if (dvid !== undefined && dvid !== null) {
+      params = params.set("dvid", dvid.toString());
+    } else {
+      params = params.set("latest", "true");
+    }
+
+    return this.http.get(`${AppSettings.getApiEndpoint()}/dataset/${did}/versionZip`, {
+      params,
+      responseType: "blob",
+    });
+  }
+
+  public retrieveAccessibleDatasets(): Observable<DashboardDataset[]> {
+    return this.http.get<DashboardDataset[]>(`${AppSettings.getApiEndpoint()}/${DATASET_LIST_URL}`);
+  }
+  public createDatasetVersion(did: number, newVersion: string): Observable<DatasetVersion> {
+    return this.http
+      .post<{
+        datasetVersion: DatasetVersion;
+        fileNodes: DatasetFileNode[];
+      }>(`${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/version/create`, newVersion, {
+        headers: { "Content-Type": "text/plain" },
+      })
+      .pipe(
+        map(response => {
+          response.datasetVersion.fileNodes = response.fileNodes;
+          return response.datasetVersion;
+        })
+      );
+  }
+
+  /**
+   * Handles multipart upload for large files using RxJS,
+   * with a concurrency limit on how many parts we process in parallel.
+   */
+  public multipartUpload(
+    datasetName: string,
+    filePath: string,
+    file: File,
+    partSize: number,
+    concurrencyLimit: number
+  ): Observable<MultipartUploadProgress> {
+    const partCount = Math.ceil(file.size / partSize);
+
+    return new Observable(observer => {
+      // Track upload progress for each part independently
+      const partProgress = new Map<number, number>();
+
+      // Progress tracking state
+      const startTime = Date.now();
+      const speedSamples: number[] = [];
+      let lastETA = 0;
+      let lastUpdateTime = 0;
+
+      // Calculate stats with smoothing
+      const calculateStats = (totalUploaded: number) => {
+        const now = Date.now();
+        const elapsed = (now - startTime) / 1000;
+
+        // Throttle updates to every 1s
+        const shouldUpdate = now - lastUpdateTime >= 1000;
+        if (!shouldUpdate) {
+          return null;
+        }
+        lastUpdateTime = now;
+
+        // Calculate speed with moving average
+        const currentSpeed = elapsed > 0 ? totalUploaded / elapsed : 0;
+        speedSamples.push(currentSpeed);
+        if (speedSamples.length > 5) speedSamples.shift();
+        const avgSpeed = speedSamples.reduce((a, b) => a + b, 0) / speedSamples.length;
+
+        // Calculate smooth ETA
+        const remaining = file.size - totalUploaded;
+        let eta = avgSpeed > 0 ? remaining / avgSpeed : 0;
+        eta = Math.min(eta, 24 * 60 * 60); // cap ETA at 24h, 86400 sec
+
+        // Smooth ETA changes (limit to 30% change)
+        if (lastETA > 0 && eta > 0) {
+          const maxChange = lastETA * 0.3;
+          const diff = Math.abs(eta - lastETA);
+          if (diff > maxChange) {
+            eta = lastETA + (eta > lastETA ? maxChange : -maxChange);
+          }
+        }
+        lastETA = eta;
+
+        // Near completion optimization
+        const percentComplete = (totalUploaded / file.size) * 100;
+        if (percentComplete > 95) {
+          eta = Math.min(eta, 10);
+        }
+
+        return {
+          uploadSpeed: avgSpeed,
+          estimatedTimeRemaining: Math.max(0, Math.round(eta)),
+          totalTime: elapsed,
+        };
+      };
+
+      const subscription = this.initiateMultipartUpload(datasetName, filePath, partCount)
+        .pipe(
+          switchMap(initiateResponse => {
+            const { uploadId, presignedUrls, physicalAddress } = initiateResponse;
+            if (!uploadId) {
+              observer.error(new Error("Failed to initiate multipart upload"));
+              return EMPTY;
+            }
+            observer.next({
+              filePath: filePath,
+              percentage: 0,
+              status: "initializing",
+              uploadId: uploadId,
+              physicalAddress: physicalAddress,
+              uploadSpeed: 0,
+              estimatedTimeRemaining: 0,
+              totalTime: 0,
+            });
+
+            // Keep track of all uploaded parts
+            const uploadedParts: { PartNumber: number; ETag: string }[] = [];
+
+            // 1) Convert presignedUrls into a stream of URLs
+            return from(presignedUrls).pipe(
+              // 2) Use mergeMap with concurrency limit to upload chunk by chunk
+              mergeMap((url, index) => {
+                const partNumber = index + 1;
+                const start = index * partSize;
+                const end = Math.min(start + partSize, file.size);
+                const chunk = file.slice(start, end);
+
+                // Upload the chunk
+                return new Observable(partObserver => {
+                  const xhr = new XMLHttpRequest();
+
+                  xhr.upload.addEventListener("progress", event => {
+                    if (event.lengthComputable) {
+                      // Update this specific part's progress
+                      partProgress.set(partNumber, event.loaded);
+
+                      // Calculate total progress across all parts
+                      let totalUploaded = 0;
+                      partProgress.forEach(bytes => (totalUploaded += bytes));
+                      const percentage = Math.round((totalUploaded / file.size) * 100);
+                      const stats = calculateStats(totalUploaded);
+
+                      observer.next({
+                        filePath,
+                        percentage: Math.min(percentage, 99), // Cap at 99% until finalized
+                        status: "uploading",
+                        uploadId,
+                        physicalAddress,
+                        ...stats,
+                      });
+                    }
+                  });
+
+                  xhr.addEventListener("load", () => {
+                    if (xhr.status === 200 || xhr.status === 201) {
+                      const etag = xhr.getResponseHeader("ETag")?.replace(/"/g, "");
+                      if (!etag) {
+                        partObserver.error(new Error(`Missing ETag for part ${partNumber}`));
+                        return;
+                      }
+
+                      // Mark this part as fully uploaded
+                      partProgress.set(partNumber, chunk.size);
+                      uploadedParts.push({ PartNumber: partNumber, ETag: etag });
+
+                      // Recalculate progress
+                      let totalUploaded = 0;
+                      partProgress.forEach(bytes => (totalUploaded += bytes));
+                      const percentage = Math.round((totalUploaded / file.size) * 100);
+                      lastUpdateTime = 0;
+                      const stats = calculateStats(totalUploaded);
+
+                      observer.next({
+                        filePath,
+                        percentage: Math.min(percentage, 99),
+                        status: "uploading",
+                        uploadId,
+                        physicalAddress,
+                        ...stats,
+                      });
+                      partObserver.complete();
+                    } else {
+                      partObserver.error(new Error(`Failed to upload part ${partNumber}`));
+                    }
+                  });
+
+                  xhr.addEventListener("error", () => {
+                    // Remove failed part from progress
+                    partProgress.delete(partNumber);
+                    partObserver.error(new Error(`Failed to upload part ${partNumber}`));
+                  });
+
+                  xhr.open("PUT", url);
+                  xhr.send(chunk);
+                });
+              }, concurrencyLimit),
+
+              // 3) Collect results from all uploads (like forkJoin, but respects concurrency)
+              toArray(),
+              // 4) Finalize if all parts succeeded
+              switchMap(() =>
+                this.finalizeMultipartUpload(datasetName, filePath, uploadId, uploadedParts, physicalAddress, false)
+              ),
+              tap(() => {
+                const finalTotalTime = (Date.now() - startTime) / 1000;
+                observer.next({
+                  filePath,
+                  percentage: 100,
+                  status: "finished",
+                  uploadId: uploadId,
+                  physicalAddress: physicalAddress,
+                  uploadSpeed: 0,
+                  estimatedTimeRemaining: 0,
+                  totalTime: finalTotalTime,
+                });
+                observer.complete();
+              }),
+              catchError((error: unknown) => {
+                // If an error occurred, abort the upload
+                const currentTotalTime = (Date.now() - startTime) / 1000;
+                observer.next({
+                  filePath,
+                  percentage: Math.round((uploadedParts.length / partCount) * 100),
+                  status: "aborted",
+                  uploadId: uploadId,
+                  physicalAddress: physicalAddress,
+                  uploadSpeed: 0,
+                  estimatedTimeRemaining: 0,
+                  totalTime: currentTotalTime,
+                });
+
+                return this.finalizeMultipartUpload(
+                  datasetName,
+                  filePath,
+                  uploadId,
+                  uploadedParts,
+                  physicalAddress,
+                  true
+                ).pipe(switchMap(() => throwError(() => error)));
+              })
+            );
+          })
+        )
+        .subscribe({
+          error: (err: unknown) => observer.error(err),
+        });
+      return () => subscription.unsubscribe();
+    });
+  }
+
+  /**
+   * Initiates a multipart upload and retrieves presigned URLs for each part.
+   * @param datasetName Dataset Name
+   * @param filePath File path within the dataset
+   * @param numParts Number of parts for the multipart upload
+   */
+  private initiateMultipartUpload(
+    datasetName: string,
+    filePath: string,
+    numParts: number
+  ): Observable<{ uploadId: string; presignedUrls: string[]; physicalAddress: string }> {
+    const params = new HttpParams()
+      .set("type", "init")
+      .set("datasetName", datasetName)
+      .set("filePath", encodeURIComponent(filePath))
+      .set("numParts", numParts.toString());
+
+    return this.http.post<{ uploadId: string; presignedUrls: string[]; physicalAddress: string }>(
+      `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/multipart-upload`,
+      {},
+      { params }
+    );
+  }
+
+  /**
+   * Completes or aborts a multipart upload, sending part numbers and ETags to the backend.
+   */
+  public finalizeMultipartUpload(
+    datasetName: string,
+    filePath: string,
+    uploadId: string,
+    parts: { PartNumber: number; ETag: string }[],
+    physicalAddress: string,
+    isAbort: boolean
+  ): Observable<Response> {
+    const params = new HttpParams()
+      .set("type", isAbort ? "abort" : "finish")
+      .set("datasetName", datasetName)
+      .set("filePath", encodeURIComponent(filePath))
+      .set("uploadId", uploadId);
+
+    return this.http.post<Response>(
+      `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/multipart-upload`,
+      { parts, physicalAddress },
+      { params }
+    );
+  }
+
+  /**
+   * Resets a dataset file difference in LakeFS.
+   * @param did Dataset ID
+   * @param filePath File path to reset
+   */
+  public resetDatasetFileDiff(did: number, filePath: string): Observable<Response> {
+    const params = new HttpParams().set("filePath", encodeURIComponent(filePath));
+
+    return this.http.put<Response>(`${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/diff`, {}, { params });
+  }
+
+  /**
+   * Deletes a dataset file from LakeFS.
+   * @param did Dataset ID
+   * @param filePath File path to delete
+   */
+  public deleteDatasetFile(did: number, filePath: string): Observable<Response> {
+    const params = new HttpParams().set("filePath", encodeURIComponent(filePath));
+
+    return this.http.delete<Response>(`${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/file`, { params });
+  }
+
+  /**
+   * Retrieves the list of uncommitted dataset changes (diffs).
+   * @param did Dataset ID
+   */
+  public getDatasetDiff(did: number): Observable<DatasetStagedObject[]> {
+    return this.http.get<DatasetStagedObject[]>(`${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/diff`);
+  }
+
+  /**
+   * retrieve a list of versions of a dataset. The list is sorted so that the latest versions are at front.
+   * @param did
+   * @param isLogin
+   */
+  public retrieveDatasetVersionList(did: number, isLogin: boolean = true): Observable<DatasetVersion[]> {
+    const apiEndPont = isLogin
+      ? `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/${DATASET_VERSION_RETRIEVE_LIST_URL}`
+      : `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/${DATASET_PUBLIC_VERSION_RETRIEVE_LIST_URL}`;
+    return this.http.get<DatasetVersion[]>(apiEndPont);
+  }
+
+  /**
+   * retrieve the latest version of a dataset.
+   * @param did
+   */
+  public retrieveDatasetLatestVersion(did: number): Observable<DatasetVersion> {
+    return this.http
+      .get<{
+        datasetVersion: DatasetVersion;
+        fileNodes: DatasetFileNode[];
+      }>(`${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/${DATASET_VERSION_LATEST_URL}`)
+      .pipe(
+        map(response => {
+          response.datasetVersion.fileNodes = response.fileNodes;
+          return response.datasetVersion;
+        })
+      );
+  }
+
+  /**
+   * retrieve a list of nodes that represent the files in the version
+   * @param did
+   * @param dvid
+   * @param isLogin
+   */
+  public retrieveDatasetVersionFileTree(
+    did: number,
+    dvid: number,
+    isLogin: boolean = true
+  ): Observable<{ fileNodes: DatasetFileNode[]; size: number }> {
+    const apiUrl = isLogin
+      ? `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/${DATASET_VERSION_BASE_URL}/${dvid}/rootFileNodes`
+      : `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/${DATASET_PUBLIC_VERSION_BASE_URL}/${dvid}/rootFileNodes`;
+    return this.http.get<{ fileNodes: DatasetFileNode[]; size: number }>(apiUrl);
+  }
+
+  public deleteDatasets(did: number): Observable<Response> {
+    return this.http.delete<Response>(`${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}`);
+  }
+
+  public updateDatasetName(did: number, name: string): Observable<Response> {
+    return this.http.post<Response>(`${AppSettings.getApiEndpoint()}/${DATASET_UPDATE_NAME_URL}`, {
+      did: did,
+      name: name,
+    });
+  }
+
+  public updateDatasetDescription(did: number, description: string): Observable<Response> {
+    return this.http.post<Response>(`${AppSettings.getApiEndpoint()}/${DATASET_UPDATE_DESCRIPTION_URL}`, {
+      did: did,
+      description: description,
+    });
+  }
+
+  public updateDatasetPublicity(did: number): Observable<Response> {
+    return this.http.post<Response>(
+      `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/${DATASET_UPDATE_PUBLICITY_URL}`,
+      {}
+    );
+  }
+
+  public updateDatasetDownloadable(did: number): Observable<Response> {
+    return this.http.post<Response>(
+      `${AppSettings.getApiEndpoint()}/${MODEL_BASE_URL}/${did}/${DATASET_UPDATE_DOWNLOADABLE_URL}`,
+      {}
+    );
+  }
+
+  public retrieveOwners(): Observable<string[]> {
+    return this.http.get<string[]>(`${AppSettings.getApiEndpoint()}/${DATASET_GET_OWNERS_URL}`);
+  }
+}
diff --git a/core/gui/src/app/dashboard/service/user/search.service.ts b/core/gui/src/app/dashboard/service/user/search.service.ts
index a48ff8991d..bc6aac5403 100644
--- a/core/gui/src/app/dashboard/service/user/search.service.ts
+++ b/core/gui/src/app/dashboard/service/user/search.service.ts
@@ -73,7 +73,7 @@ export class SearchService {
     params: SearchFilterParameters,
     start: number,
     count: number,
-    type: "workflow" | "project" | "file" | "dataset" | null,
+    type: "workflow" | "project" | "file" | "dataset" | "model" | null,
     orderBy: SortMethod,
     isLogin: boolean,
     includePublic: boolean = false
@@ -125,17 +125,18 @@ export class SearchService {
     params: SearchFilterParameters,
     start: number,
     count: number,
-    type: "workflow" | "project" | "dataset" | "file" | null,
+    type: "workflow" | "project" | "dataset" | "file" | "model"| null,
     orderBy: SortMethod,
     isLogin: boolean,
     includePublic: boolean
   ): Observable<SearchResultBatch> {
     return this.search(keywords, params, start, count, type, orderBy, isLogin, includePublic).pipe(
       switchMap(results => {
-        const hasMismatch = type === "dataset" ? results.hasMismatch ?? false : undefined;
+        console.log("Search results:", results);
+        const hasMismatch = (type === "dataset" || type==="model") ? results.hasMismatch ?? false : undefined;
         const filteredResults =
-          type === "dataset" ? results.results.filter(i => i !== null && i.dataset != null) : results.results;
-
+          (type === "dataset") ? results.results.filter(i => i !== null && i.dataset != null) : (type === "model") ? results.results.filter(i => i !== null && i.model != null): results.results;
+        console.log("Filtered results:", filteredResults);
         return this.extendSearchResultsWithHubActivityInfo(filteredResults, isLogin).pipe(
           map(entries => ({
             entries,
@@ -173,6 +174,7 @@ export class SearchService {
       if (i.project) userIds.add(i.project.ownerId);
       else if (i.workflow) userIds.add(i.workflow.ownerId);
       else if (i.dataset?.dataset?.ownerUid != null) userIds.add(i.dataset.dataset.ownerUid);
+      else if (i.model?.model?.ownerUid != null) userIds.add(i.model.model.ownerUid);
     });
     const userInfo$ = userIds.size ? this.getUserInfo(Array.from(userIds)) : of({} as Record<number, UserInfo>);
 
@@ -188,7 +190,11 @@ export class SearchService {
       } else if (i.dataset?.dataset?.did != null) {
         entityTypes.push(EntityType.Dataset);
         entityIds.push(i.dataset.dataset.did);
+      } else if (i.model?.model?.mid != null) {
+        entityTypes.push(EntityType.Model);
+        entityIds.push(i.model.model.mid);
       }
+
     });
 
     const counts$ =
@@ -224,14 +230,19 @@ export class SearchService {
             ? new DashboardEntry(i.workflow)
             : i.project
               ? new DashboardEntry(i.project)
-              : new DashboardEntry(i.dataset!);
+              :  i.dataset
+                ? new DashboardEntry(i.dataset)
+                : new DashboardEntry(i.model!);
 
           const key = `${entry.type}:${entry.id}`;
           const ownerId = i.workflow
             ? i.workflow.ownerId
             : i.project
               ? i.project.ownerId
-              : i.dataset!.dataset!.ownerUid!;
+              : i.dataset
+                ? i.dataset.dataset.ownerUid!
+                : i.model!.model.ownerUid!;
+
           const ui = (userMap as any)[ownerId];
           if (ui) {
             entry.setOwnerName(ui.userName);
diff --git a/core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts b/core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts
index 2ea0058264..d739bb43de 100644
--- a/core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts
+++ b/core/gui/src/app/dashboard/type/dashboard-dataset.interface.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { Dataset, DatasetVersion } from "../../common/type/dataset";
+import { Dataset, Model, DatasetVersion } from "../../common/type/dataset";
 import { DatasetFileNode } from "../../common/type/datasetVersionFileTree";
 
 export interface DashboardDataset {
@@ -27,3 +27,12 @@ export interface DashboardDataset {
   accessPrivilege: "READ" | "WRITE" | "NONE";
   size: number;
 }
+
+export interface DashboardModel{
+  isOwner: boolean;
+  ownerEmail: string;
+  model: Model;
+  accessPrivilege: "READ" | "WRITE" | "NONE";
+  size: number;
+}
+
diff --git a/core/gui/src/app/dashboard/type/dashboard-entry.ts b/core/gui/src/app/dashboard/type/dashboard-entry.ts
index e526ea01ba..1ba86eea88 100644
--- a/core/gui/src/app/dashboard/type/dashboard-entry.ts
+++ b/core/gui/src/app/dashboard/type/dashboard-entry.ts
@@ -20,8 +20,14 @@
 import { DashboardFile } from "./dashboard-file.interface";
 import { DashboardWorkflow } from "./dashboard-workflow.interface";
 import { DashboardProject } from "./dashboard-project.interface";
-import { DashboardDataset } from "./dashboard-dataset.interface";
-import { isDashboardDataset, isDashboardFile, isDashboardProject, isDashboardWorkflow } from "./type-predicates";
+import { DashboardDataset, DashboardModel } from "./dashboard-dataset.interface";
+import {
+  isDashboardDataset,
+  isDashboardFile,
+  isDashboardModel,
+  isDashboardProject,
+  isDashboardWorkflow,
+} from "./type-predicates";
 import { EntityType } from "../../hub/service/hub.service";
 
 export interface UserInfo {
@@ -49,7 +55,7 @@ export class DashboardEntry {
   isLiked: boolean;
   accessibleUserIds: number[];
 
-  constructor(public value: DashboardWorkflow | DashboardProject | DashboardFile | DashboardDataset) {
+  constructor(public value: DashboardWorkflow | DashboardProject | DashboardFile | DashboardDataset | DashboardModel) {
     if (isDashboardWorkflow(value)) {
       this.type = EntityType.Workflow;
       this.id = value.workflow.wid;
@@ -122,6 +128,24 @@ export class DashboardEntry {
       this.likeCount = 0;
       this.isLiked = false;
       this.accessibleUserIds = [];
+    } else if (isDashboardModel(value)) {
+      this.type = EntityType.Model;
+      this.id = value.model.mid;
+      this.name = value.model.name;
+      this.description = value.model.description;
+      this.creationTime = value.model.creationTime;
+      this.lastModifiedTime = value.model.creationTime;
+      this.accessLevel = value.accessPrivilege;
+      this.ownerName = "";
+      this.ownerEmail = value.ownerEmail;
+      this.ownerGoogleAvatar = "";
+      this.ownerId = value.model.ownerUid;
+      this.size = value.size;
+      this.viewCount = 0;
+      this.cloneCount = 0;
+      this.likeCount = 0;
+      this.isLiked = false;
+      this.accessibleUserIds = [];
     } else {
       throw new Error("Unexpected type in DashboardEntry.");
     }
@@ -180,4 +204,11 @@ export class DashboardEntry {
     }
     return this.value;
   }
+
+  get model(): DashboardModel {
+    if (!isDashboardModel(this.value)) {
+      throw new Error("Value is not of type DashboardModel");
+    }
+    return this.value;
+  }
 }
diff --git a/core/gui/src/app/dashboard/type/search-filter-parameters.ts b/core/gui/src/app/dashboard/type/search-filter-parameters.ts
index 848bec2619..4eb5511805 100644
--- a/core/gui/src/app/dashboard/type/search-filter-parameters.ts
+++ b/core/gui/src/app/dashboard/type/search-filter-parameters.ts
@@ -37,7 +37,7 @@ export const toQueryStrings = (
   params: SearchFilterParameters,
   start?: number,
   count?: number,
-  type?: "workflow" | "project" | "file" | "dataset" | null,
+  type?: "workflow" | "project" | "file" | "dataset" | "model" | null,
   orderBy?: SortMethod
 ): string => {
   function* getQueryParameters(): Iterable<[name: string, value: string]> {
diff --git a/core/gui/src/app/dashboard/type/search-result.ts b/core/gui/src/app/dashboard/type/search-result.ts
index 268381b0a5..5720618cd8 100644
--- a/core/gui/src/app/dashboard/type/search-result.ts
+++ b/core/gui/src/app/dashboard/type/search-result.ts
@@ -20,15 +20,16 @@
 import { DashboardFile } from "./dashboard-file.interface";
 import { DashboardWorkflow } from "./dashboard-workflow.interface";
 import { DashboardProject } from "./dashboard-project.interface";
-import { DashboardDataset } from "./dashboard-dataset.interface";
+import { DashboardDataset, DashboardModel } from "./dashboard-dataset.interface";
 import { DashboardEntry } from "./dashboard-entry";
 
 export interface SearchResultItem {
-  resourceType: "workflow" | "project" | "file" | "dataset";
+  resourceType: "workflow" | "project" | "file" | "dataset" | "model";
   workflow?: DashboardWorkflow;
   project?: DashboardProject;
   file?: DashboardFile;
   dataset?: DashboardDataset;
+  model?: DashboardModel;
 }
 
 export interface SearchResult {
diff --git a/core/gui/src/app/dashboard/type/type-predicates.ts b/core/gui/src/app/dashboard/type/type-predicates.ts
index 6c9cf96263..53c7815940 100644
--- a/core/gui/src/app/dashboard/type/type-predicates.ts
+++ b/core/gui/src/app/dashboard/type/type-predicates.ts
@@ -20,7 +20,7 @@
 import { DashboardWorkflow } from "./dashboard-workflow.interface";
 import { DashboardProject } from "./dashboard-project.interface";
 import { DashboardFile } from "./dashboard-file.interface";
-import { DashboardDataset } from "./dashboard-dataset.interface";
+import { DashboardDataset, DashboardModel } from "./dashboard-dataset.interface";
 
 export function isDashboardWorkflow(value: any): value is DashboardWorkflow {
   return value && typeof value.workflow === "object";
@@ -37,3 +37,7 @@ export function isDashboardFile(value: any): value is DashboardFile {
 export function isDashboardDataset(value: any): value is DashboardDataset {
   return value && typeof value.dataset === "object";
 }
+
+export function isDashboardModel(value: any): value is DashboardModel {
+  return value && typeof value.model === "object";
+}
diff --git a/core/gui/src/app/hub/service/hub.service.ts b/core/gui/src/app/hub/service/hub.service.ts
index 231fb5e3b8..5bdd9eee75 100644
--- a/core/gui/src/app/hub/service/hub.service.ts
+++ b/core/gui/src/app/hub/service/hub.service.ts
@@ -30,6 +30,7 @@ export enum EntityType {
   Dataset = "dataset",
   Project = "project",
   File = "file",
+  Model = "model",
 }
 
 export enum ActionType {
diff --git a/core/scripts/sql/texera_ddl.sql b/core/scripts/sql/texera_ddl.sql
index 7c6cf15c7c..906b6a2ab3 100644
--- a/core/scripts/sql/texera_ddl.sql
+++ b/core/scripts/sql/texera_ddl.sql
@@ -50,6 +50,9 @@ DROP TABLE IF EXISTS workflow_executions CASCADE;
 DROP TABLE IF EXISTS dataset CASCADE;
 DROP TABLE IF EXISTS dataset_user_access CASCADE;
 DROP TABLE IF EXISTS dataset_version CASCADE;
+DROP TABLE IF EXISTS model CASCADE;
+DROP TABLE IF EXISTS model_user_access CASCADE;
+DROP TABLE IF EXISTS model_version CASCADE;
 DROP TABLE IF EXISTS public_project CASCADE;
 DROP TABLE IF EXISTS project_user_access CASCADE;
 DROP TABLE IF EXISTS workflow_user_likes CASCADE;
@@ -59,6 +62,8 @@ DROP TABLE IF EXISTS workflow_user_activity CASCADE;
 DROP TABLE IF EXISTS user_activity CASCADE;
 DROP TABLE IF EXISTS dataset_user_likes CASCADE;
 DROP TABLE IF EXISTS dataset_view_count CASCADE;
+DROP TABLE IF EXISTS model_user_likes CASCADE;
+DROP TABLE IF EXISTS model_view_count CASCADE;
 DROP TABLE IF EXISTS site_settings CASCADE;
 DROP TABLE IF EXISTS computing_unit_user_access CASCADE;
 
@@ -260,6 +265,43 @@ CREATE TABLE IF NOT EXISTS dataset_version
     FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE
     );
 
+-- model
+CREATE TABLE IF NOT EXISTS model
+(
+    mid            SERIAL PRIMARY KEY,
+    owner_uid      INT NOT NULL,
+    name           VARCHAR(128) NOT NULL,
+    is_public      BOOLEAN NOT NULL DEFAULT TRUE,
+    is_downloadable BOOLEAN NOT NULL DEFAULT TRUE,
+    description    VARCHAR(512) NOT NULL,
+    creation_time  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (owner_uid) REFERENCES "user"(uid) ON DELETE CASCADE
+);
+
+-- model_user_access
+CREATE TABLE IF NOT EXISTS model_user_access
+(
+    mid       INT NOT NULL,
+    uid       INT NOT NULL,
+    privilege privilege_enum NOT NULL DEFAULT 'NONE',
+    PRIMARY KEY (mid, uid),
+    FOREIGN KEY (mid) REFERENCES model(mid) ON DELETE CASCADE,
+    FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE
+);
+
+-- model_version
+CREATE TABLE IF NOT EXISTS model_version
+(
+    mvid          SERIAL PRIMARY KEY,
+    mid           INT NOT NULL,
+    creator_uid   INT NOT NULL,
+    name          VARCHAR(128) NOT NULL,
+    version_hash  VARCHAR(64) NOT NULL,
+    creation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (mid) REFERENCES model(mid) ON DELETE CASCADE
+);
+
+
 -- operator_executions (modified to match MySQL: no separate primary key; added console_messages_uri)
 CREATE TABLE IF NOT EXISTS operator_executions
 (
@@ -341,6 +383,25 @@ CREATE TABLE IF NOT EXISTS dataset_view_count
     FOREIGN KEY (did) REFERENCES dataset(did) ON DELETE CASCADE
     );
 
+-- model_user_likes table
+CREATE TABLE IF NOT EXISTS model_user_likes
+(
+    uid INTEGER NOT NULL,
+    mid INTEGER NOT NULL,
+    PRIMARY KEY (uid, mid),
+    FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE,
+    FOREIGN KEY (mid) REFERENCES model(mid) ON DELETE CASCADE
+);
+
+-- model_view_count table
+CREATE TABLE IF NOT EXISTS model_view_count
+(
+    mid        INTEGER NOT NULL,
+    view_count INTEGER NOT NULL DEFAULT 0,
+    PRIMARY KEY (mid),
+    FOREIGN KEY (mid) REFERENCES model(mid) ON DELETE CASCADE
+);
+
 -- site_settings table
 CREATE TABLE IF NOT EXISTS site_settings
 (