Skip to content

Commit c4b7325

Browse files
GspikeHaloMA77HEW820
authored andcommitted
Add public datasets under "Hub" (#3129)
### Purpose: Added a new interface for public datasets, allowing non-logged-in users to view datasets shared by others. The behavior of public datasets aligns with that of public workflows, ensuring consistent access and interaction with public resources. ### Changes: 1. Added "Hub Dataset" to the left sidebar. Clicking on it will display public datasets. 2. Updated the list item behavior: When a user clicks a list item of type "dataset," different navigation actions are taken depending on the situation: - If the user is not logged in, they will always be redirected to the Hub Dataset page. This page disables the download functionality and does not display the publish switch. - If the user is logged in but lacks direct access permissions (e.g., read or write), they will still be redirected to the Hub Dataset page. - If the user is logged in and has read or write permissions, they will be redirected to the User Dataset page, which is the original dataset page we had. ### Demos: **Before:** Left sidebar: ![image](https://github.com/user-attachments/assets/4d461f79-1a5d-4e65-b6d9-08c55005410d) **After:** Left sidebar: ![image](https://github.com/user-attachments/assets/a468cbe7-f40f-40f1-8458-4b5c39ad056a) Public Dataset Interface Before Login: ![image](https://github.com/user-attachments/assets/89819585-e401-43f6-82bb-8fcb50dace1f) Public Dataset Interface After Login: ![image](https://github.com/user-attachments/assets/0c0ad66f-764d-4261-9137-0ae433cfd38a) Non-logged-in user clicking any dataset: ![image](https://github.com/user-attachments/assets/04643d12-57af-4b94-bbb6-331db13ae038) Logged-in user without direct permissions clicking any dataset: ![image](https://github.com/user-attachments/assets/ce693abc-a6b3-47cc-8b5f-63c0b5e24f62) Logged-in user with direct permissions clicking any dataset: ![image](https://github.com/user-attachments/assets/df28fd0b-9992-4e40-b480-ec83bfe3dc9d)
1 parent 66e0b6a commit c4b7325

22 files changed

+334
-164
lines changed

core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/DashboardResource.scala

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -202,18 +202,4 @@ class DashboardResource {
202202

203203
userIdToInfoMap
204204
}
205-
206-
@GET
207-
@Path("/workflowUserAccess")
208-
def workflowUserAccess(
209-
@QueryParam("wid") wid: UInteger
210-
): util.List[UInteger] = {
211-
val records = context
212-
.select(WORKFLOW_USER_ACCESS.UID)
213-
.from(WORKFLOW_USER_ACCESS)
214-
.where(WORKFLOW_USER_ACCESS.WID.eq(wid))
215-
.fetch()
216-
217-
records.getValues(WORKFLOW_USER_ACCESS.UID)
218-
}
219205
}

core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/hub/workflow/HubWorkflowResource.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,4 +404,18 @@ class HubWorkflowResource {
404404
.where(WORKFLOW_VIEW_COUNT.WID.eq(wid))
405405
.fetchOneInto(classOf[Int])
406406
}
407+
408+
@GET
409+
@Path("/workflowUserAccess")
410+
def workflowUserAccess(
411+
@QueryParam("wid") wid: UInteger
412+
): util.List[UInteger] = {
413+
val records = context
414+
.select(WORKFLOW_USER_ACCESS.UID)
415+
.from(WORKFLOW_USER_ACCESS)
416+
.where(WORKFLOW_USER_ACCESS.WID.eq(wid))
417+
.fetch()
418+
419+
records.getValues(WORKFLOW_USER_ACCESS.UID)
420+
}
407421
}

core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/dataset/DatasetAccessResource.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ object DatasetAccessResource {
2626
.getInstance(StorageConfig.jdbcUrl, StorageConfig.jdbcUsername, StorageConfig.jdbcPassword)
2727
.createDSLContext()
2828

29-
def userHasReadAccess(ctx: DSLContext, did: UInteger, uid: UInteger): Boolean = {
29+
def isDatasetPublic(ctx: DSLContext, did: UInteger): Boolean = {
3030
val datasetDao = new DatasetDao(ctx.configuration())
31-
val isDatasetPublic = Option(datasetDao.fetchOneByDid(did))
31+
Option(datasetDao.fetchOneByDid(did))
3232
.flatMap(dataset => Option(dataset.getIsPublic))
3333
.contains(1.toByte)
34+
}
3435

35-
isDatasetPublic ||
36+
def userHasReadAccess(ctx: DSLContext, did: UInteger, uid: UInteger): Boolean = {
37+
isDatasetPublic(ctx, did) ||
3638
userHasWriteAccess(ctx, did, uid) ||
3739
getDatasetUserAccessPrivilege(ctx, did, uid) == DatasetUserAccessPrivilege.READ
3840
}
@@ -92,7 +94,7 @@ class DatasetAccessResource {
9294
@GET
9395
@Path("/owner/{did}")
9496
def getOwnerEmailOfDataset(@PathParam("did") did: UInteger): String = {
95-
var email = "";
97+
var email = ""
9698
withTransaction(context) { ctx =>
9799
val owner = getOwner(ctx, did)
98100
if (owner != null) {

core/amber/src/main/scala/edu/uci/ics/texera/web/resource/dashboard/user/dataset/DatasetResource.scala

Lines changed: 132 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,6 @@ object DatasetResource {
345345
}
346346

347347
@Produces(Array(MediaType.APPLICATION_JSON, "image/jpeg", "application/pdf"))
348-
@RolesAllowed(Array("REGULAR", "ADMIN"))
349348
@Path("/dataset")
350349
class DatasetResource {
351350
private val ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE = "User has no read access to this dataset"
@@ -359,20 +358,27 @@ class DatasetResource {
359358
private def getDashboardDataset(
360359
ctx: DSLContext,
361360
did: UInteger,
362-
uid: UInteger
361+
uid: Option[UInteger],
362+
isPublic: Boolean = false
363363
): DashboardDataset = {
364-
if (!userHasReadAccess(ctx, did, uid)) {
364+
if (
365+
(isPublic && !isDatasetPublic(ctx, did)) ||
366+
(!isPublic && (!userHasReadAccess(ctx, did, uid.get)))
367+
) {
365368
throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
366369
}
367370

368371
val targetDataset = getDatasetByID(ctx, did)
369-
val userAccessPrivilege = getDatasetUserAccessPrivilege(ctx, did, uid)
372+
val userAccessPrivilege =
373+
if (isPublic) DatasetUserAccessPrivilege.NONE
374+
else getDatasetUserAccessPrivilege(ctx, did, uid.get)
375+
val isOwner = !isPublic && (targetDataset.getOwnerUid == uid.get)
370376

371377
DashboardDataset(
372378
targetDataset,
373379
getOwner(ctx, did).getEmail,
374380
userAccessPrivilege,
375-
targetDataset.getOwnerUid == uid,
381+
isOwner,
376382
List(),
377383
calculateDatasetVersionSize(did)
378384
)
@@ -401,6 +407,7 @@ class DatasetResource {
401407
}
402408

403409
@POST
410+
@RolesAllowed(Array("REGULAR", "ADMIN"))
404411
@Path("/create")
405412
@Consumes(Array(MediaType.MULTIPART_FORM_DATA))
406413
def createDataset(
@@ -477,6 +484,7 @@ class DatasetResource {
477484
}
478485

479486
@POST
487+
@RolesAllowed(Array("REGULAR", "ADMIN"))
480488
@Path("/delete")
481489
def deleteDataset(datasetIDs: DatasetIDs, @Auth user: SessionUser): Response = {
482490
val uid = user.getUid
@@ -501,6 +509,7 @@ class DatasetResource {
501509
@POST
502510
@Consumes(Array(MediaType.APPLICATION_JSON))
503511
@Produces(Array(MediaType.APPLICATION_JSON))
512+
@RolesAllowed(Array("REGULAR", "ADMIN"))
504513
@Path("/update/name")
505514
def updateDatasetName(
506515
modificator: DatasetNameModification,
@@ -525,6 +534,7 @@ class DatasetResource {
525534
@POST
526535
@Consumes(Array(MediaType.APPLICATION_JSON))
527536
@Produces(Array(MediaType.APPLICATION_JSON))
537+
@RolesAllowed(Array("REGULAR", "ADMIN"))
528538
@Path("/update/description")
529539
def updateDatasetDescription(
530540
modificator: DatasetDescriptionModification,
@@ -548,6 +558,7 @@ class DatasetResource {
548558
}
549559

550560
@POST
561+
@RolesAllowed(Array("REGULAR", "ADMIN"))
551562
@Path("/{did}/update/publicity")
552563
def toggleDatasetPublicity(
553564
@PathParam("did") did: UInteger,
@@ -574,6 +585,7 @@ class DatasetResource {
574585
}
575586

576587
@POST
588+
@RolesAllowed(Array("REGULAR", "ADMIN"))
577589
@Path("/{did}/version/create")
578590
@Consumes(Array(MediaType.MULTIPART_FORM_DATA))
579591
def createDatasetVersion(
@@ -607,6 +619,7 @@ class DatasetResource {
607619
* @return list of user accessible DashboardDataset objects
608620
*/
609621
@GET
622+
@RolesAllowed(Array("REGULAR", "ADMIN"))
610623
@Path("")
611624
def listDatasets(
612625
@Auth user: SessionUser
@@ -684,28 +697,36 @@ class DatasetResource {
684697
}
685698

686699
@GET
700+
@RolesAllowed(Array("REGULAR", "ADMIN"))
687701
@Path("/{did}/version/list")
688702
def getDatasetVersionList(
689703
@PathParam("did") did: UInteger,
690704
@Auth user: SessionUser
691705
): List[DatasetVersion] = {
692706
val uid = user.getUid
693707
withTransaction(context)(ctx => {
694-
695708
if (!userHasReadAccess(ctx, did, uid)) {
696709
throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
697710
}
698-
val result: java.util.List[DatasetVersion] = ctx
699-
.selectFrom(DATASET_VERSION)
700-
.where(DATASET_VERSION.DID.eq(did))
701-
.orderBy(DATASET_VERSION.CREATION_TIME.desc()) // or .asc() for ascending
702-
.fetchInto(classOf[DatasetVersion])
711+
fetchDatasetVersions(ctx, did)
712+
})
713+
}
703714

704-
result.asScala.toList
715+
@GET
716+
@Path("/{did}/publicVersion/list")
717+
def getPublicDatasetVersionList(
718+
@PathParam("did") did: UInteger
719+
): List[DatasetVersion] = {
720+
withTransaction(context)(ctx => {
721+
if (!isDatasetPublic(ctx, did)) {
722+
throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
723+
}
724+
fetchDatasetVersions(ctx, did)
705725
})
706726
}
707727

708728
@GET
729+
@RolesAllowed(Array("REGULAR", "ADMIN"))
709730
@Path("/{did}/version/latest")
710731
def retrieveLatestDatasetVersion(
711732
@PathParam("did") did: UInteger,
@@ -753,65 +774,53 @@ class DatasetResource {
753774
}
754775

755776
@GET
777+
@RolesAllowed(Array("REGULAR", "ADMIN"))
756778
@Path("/{did}/version/{dvid}/rootFileNodes")
757779
def retrieveDatasetVersionRootFileNodes(
758780
@PathParam("did") did: UInteger,
759781
@PathParam("dvid") dvid: UInteger,
760782
@Auth user: SessionUser
761783
): DatasetVersionRootFileNodesResponse = {
762784
val uid = user.getUid
785+
withTransaction(context)(ctx =>
786+
fetchDatasetVersionRootFileNodes(ctx, did, dvid, Some(uid), isPublic = false)
787+
)
788+
}
763789

764-
withTransaction(context)(ctx => {
765-
val dataset = getDashboardDataset(ctx, did, uid)
766-
val targetDatasetPath = PathUtils.getDatasetPath(did)
767-
val datasetVersion = getDatasetVersionByID(ctx, dvid)
768-
val datasetName = dataset.dataset.getName
769-
val fileNodes = GitVersionControlLocalFileStorage.retrieveRootFileNodesOfVersion(
770-
targetDatasetPath,
771-
datasetVersion.getVersionHash
772-
)
773-
val versionHash = getDatasetVersionByID(ctx, dvid).getVersionHash
774-
val size = calculateDatasetVersionSize(did, Some(versionHash))
775-
val ownerFileNode = DatasetFileNode
776-
.fromPhysicalFileNodes(
777-
Map((dataset.ownerEmail, datasetName, datasetVersion.getName) -> fileNodes.asScala.toList)
778-
)
779-
.head
780-
781-
DatasetVersionRootFileNodesResponse(
782-
ownerFileNode.children.get
783-
.find(_.getName == datasetName)
784-
.head
785-
.children
786-
.get
787-
.find(_.getName == datasetVersion.getName)
788-
.head
789-
.children
790-
.get,
791-
size
792-
)
793-
})
790+
@GET
791+
@Path("/{did}/publicVersion/{dvid}/rootFileNodes")
792+
def retrievePublicDatasetVersionRootFileNodes(
793+
@PathParam("did") did: UInteger,
794+
@PathParam("dvid") dvid: UInteger
795+
): DatasetVersionRootFileNodesResponse = {
796+
withTransaction(context)(ctx =>
797+
fetchDatasetVersionRootFileNodes(ctx, did, dvid, None, isPublic = true)
798+
)
794799
}
795800

796801
@GET
802+
@RolesAllowed(Array("REGULAR", "ADMIN"))
797803
@Path("/{did}")
798804
def getDataset(
799805
@PathParam("did") did: UInteger,
800806
@Auth user: SessionUser
801807
): DashboardDataset = {
802808
val uid = user.getUid
803-
withTransaction(context)(ctx => {
804-
val dashboardDataset = getDashboardDataset(ctx, did, uid)
805-
val size = calculateDatasetVersionSize(did)
806-
dashboardDataset.copy(size = size)
807-
})
809+
withTransaction(context)(ctx => fetchDataset(ctx, did, Some(uid), isPublic = false))
810+
}
811+
812+
@GET
813+
@Path("/public/{did}")
814+
def getPublicDataset(
815+
@PathParam("did") did: UInteger
816+
): DashboardDataset = {
817+
withTransaction(context)(ctx => fetchDataset(ctx, did, None, isPublic = true))
808818
}
809819

810820
@GET
811821
@Path("/file")
812822
def retrieveDatasetSingleFile(
813-
@QueryParam("path") pathStr: String,
814-
@Auth user: SessionUser
823+
@QueryParam("path") pathStr: String
815824
): Response = {
816825
val decodedPathStr = URLDecoder.decode(pathStr, StandardCharsets.UTF_8.name())
817826

@@ -863,6 +872,7 @@ class DatasetResource {
863872
* @return A Response containing the dataset version as a ZIP file.
864873
*/
865874
@GET
875+
@RolesAllowed(Array("REGULAR", "ADMIN"))
866876
@Path("/version-zip")
867877
def retrieveDatasetVersionZip(
868878
@QueryParam("did") did: UInteger,
@@ -935,4 +945,77 @@ class DatasetResource {
935945
.`type`("application/zip")
936946
.build()
937947
}
948+
949+
@GET
950+
@Path("/datasetUserAccess")
951+
def datasetUserAccess(
952+
@QueryParam("did") did: UInteger
953+
): java.util.List[UInteger] = {
954+
val records = context
955+
.select(DATASET_USER_ACCESS.UID)
956+
.from(DATASET_USER_ACCESS)
957+
.where(DATASET_USER_ACCESS.DID.eq(did))
958+
.fetch()
959+
960+
records.getValues(DATASET_USER_ACCESS.UID)
961+
}
962+
963+
private def fetchDatasetVersions(ctx: DSLContext, did: UInteger): List[DatasetVersion] = {
964+
ctx
965+
.selectFrom(DATASET_VERSION)
966+
.where(DATASET_VERSION.DID.eq(did))
967+
.orderBy(DATASET_VERSION.CREATION_TIME.desc()) // Change to .asc() for ascending order
968+
.fetchInto(classOf[DatasetVersion])
969+
.asScala
970+
.toList
971+
}
972+
973+
private def fetchDatasetVersionRootFileNodes(
974+
ctx: DSLContext,
975+
did: UInteger,
976+
dvid: UInteger,
977+
uid: Option[UInteger],
978+
isPublic: Boolean
979+
): DatasetVersionRootFileNodesResponse = {
980+
val dataset = getDashboardDataset(ctx, did, uid, isPublic)
981+
val targetDatasetPath = PathUtils.getDatasetPath(did)
982+
val datasetVersion = getDatasetVersionByID(ctx, dvid)
983+
val datasetName = dataset.dataset.getName
984+
val fileNodes = GitVersionControlLocalFileStorage.retrieveRootFileNodesOfVersion(
985+
targetDatasetPath,
986+
datasetVersion.getVersionHash
987+
)
988+
val versionHash = datasetVersion.getVersionHash
989+
val size = calculateDatasetVersionSize(did, Some(versionHash))
990+
991+
val ownerFileNode = DatasetFileNode
992+
.fromPhysicalFileNodes(
993+
Map((dataset.ownerEmail, datasetName, datasetVersion.getName) -> fileNodes.asScala.toList)
994+
)
995+
.head
996+
997+
DatasetVersionRootFileNodesResponse(
998+
ownerFileNode.children.get
999+
.find(_.getName == datasetName)
1000+
.head
1001+
.children
1002+
.get
1003+
.find(_.getName == datasetVersion.getName)
1004+
.head
1005+
.children
1006+
.get,
1007+
size
1008+
)
1009+
}
1010+
1011+
private def fetchDataset(
1012+
ctx: DSLContext,
1013+
did: UInteger,
1014+
uid: Option[UInteger],
1015+
isPublic: Boolean
1016+
): DashboardDataset = {
1017+
val dashboardDataset = getDashboardDataset(ctx, did, uid, isPublic)
1018+
val size = calculateDatasetVersionSize(did)
1019+
dashboardDataset.copy(size = size)
1020+
}
9381021
}

core/gui/src/app/app-routing.constant.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export const DASHBOARD_HUB = `${DASHBOARD}/hub`;
66
export const DASHBOARD_HUB_WORKFLOW = `${DASHBOARD_HUB}/workflow`;
77
export const DASHBOARD_HUB_WORKFLOW_RESULT = `${DASHBOARD_HUB_WORKFLOW}/result`;
88
export const DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL = `${DASHBOARD_HUB_WORKFLOW_RESULT}/detail`;
9+
export const DASHBOARD_HUB_DATASET = `${DASHBOARD_HUB}/dataset`;
10+
export const DASHBOARD_HUB_DATASET_RESULT = `${DASHBOARD_HUB_DATASET}/result`;
11+
export const DASHBOARD_HUB_DATASET_RESULT_DETAIL = `${DASHBOARD_HUB_DATASET_RESULT}/detail`;
912

1013
export const DASHBOARD_USER = `${DASHBOARD}/user`;
1114
export const DASHBOARD_USER_PROJECT = `${DASHBOARD_USER}/project`;

0 commit comments

Comments
 (0)