Skip to content

Commit 5c63272

Browse files
todd_nlmarini
authored andcommitted
New Tree view as a tab in home page to navigate resources as a hiearchical tree (spaces, collections, datasets, folders and files). The tree is lazily loaded using a new endpoint api/tree/getChildrenOfNode.
1 parent 78d815f commit 5c63272

File tree

18 files changed

+11294
-1
lines changed

18 files changed

+11294
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ to run Clowder with the `MONGOUPDATE` flag set to update the database.**
4242
- Support for MongoDB 3.6 and below. This required the removal of aggregators which can result in
4343
operations taking a little longer. This is needed to support Clowder as a Kubernetes Helm chart.
4444
[CATS-806](https://opensource.ncsa.illinois.edu/jira/browse/CATS-806)
45+
- New Tree view as a tab in home page to navigate resources as a hiearchical tree (spaces, collections, datasets,
46+
folders and files). The tree is lazily loaded using a new endpoint `api/tree/getChildrenOfNode`.
4547

4648
### Fixed
4749
- Downloading metrics reports would fail due to timeout on large databases. Report CSVs are now streamed

app/api/Tree.scala

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package api
2+
3+
import javax.inject.{Inject, Singleton}
4+
import play.api.libs.json.Json.toJson
5+
import services._
6+
7+
@Singleton
8+
class Tree @Inject() (
9+
treeService: TreeService) extends ApiController {
10+
11+
def getChildrenOfNode(nodeType: String, nodeId: Option[String], mine: Boolean) = PrivateServerAction { implicit request =>
12+
request.user match {
13+
case Some(user) => {
14+
var result = treeService.getChildrenOfNode(nodeType,nodeId,mine,user)
15+
Ok(toJson(result))
16+
}
17+
case None => BadRequest("No user supplied")
18+
}
19+
}
20+
21+
}

app/controllers/Application.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ class Application @Inject() (files: FileService, collections: CollectionService,
435435
api.routes.javascript.Spaces.acceptRequest,
436436
api.routes.javascript.Spaces.rejectRequest,
437437
api.routes.javascript.Spaces.verifySpace,
438+
api.routes.javascript.Tree.getChildrenOfNode,
438439
api.routes.javascript.Users.getUser,
439440
api.routes.javascript.Users.findById,
440441
api.routes.javascript.Users.follow,
@@ -492,6 +493,7 @@ class Application @Inject() (files: FileService, collections: CollectionService,
492493
api.routes.javascript.Folders.moveFileBetweenFolders,
493494
api.routes.javascript.Folders.moveFileToDataset,
494495
api.routes.javascript.Thumbnails.get,
496+
api.routes.javascript.Tree.getChildrenOfNode,
495497
controllers.routes.javascript.Login.isLoggedIn,
496498
controllers.routes.javascript.Login.ldapAuthenticate,
497499
controllers.routes.javascript.Files.file,

app/services/TreeService.scala

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
package services
2+
3+
import api.Permission
4+
import api.Permission.Permission
5+
import javax.inject.{Inject, Singleton}
6+
import models._
7+
import play.api.libs.json.{JsValue, Json}
8+
9+
import scala.collection.immutable.List
10+
import scala.collection.mutable.ListBuffer
11+
12+
/**
13+
* Service for creating a file tree view for Spaces, Collections, Datasets.
14+
*
15+
*
16+
*/
17+
18+
class TreeService @Inject()(
19+
datasets: DatasetService,
20+
fileService: FileService,
21+
folderService: FolderService,
22+
collections: CollectionService,
23+
userService: UserService,
24+
spaceService: SpaceService,
25+
events:EventService
26+
) {
27+
28+
def getChildrenOfNode(nodeType: String, nodeId: Option[String], mine: Boolean, user: User): List[JsValue] = {
29+
var children : List[JsValue] = List.empty[JsValue]
30+
if (nodeType == "space"){
31+
nodeId match {
32+
case Some(id) => {
33+
spaceService.get(UUID(id)) match {
34+
case Some(space) => {
35+
children = getChildrenOfSpace(Some(space), mine, user)
36+
}
37+
case None =>
38+
}
39+
}
40+
case None => children = getSpaces(mine, user)
41+
}
42+
} else if (nodeType == "collection") {
43+
nodeId match {
44+
case Some(id) => {
45+
collections.get(UUID(id)) match {
46+
case Some(col) => {
47+
children = getChildrenOfCollection(Some(col), mine, user)
48+
}
49+
case None =>
50+
}
51+
}
52+
case None => children = getCollections(mine, user)
53+
}
54+
} else if (nodeType == "dataset") {
55+
nodeId match {
56+
case Some(id) => {
57+
datasets.get(UUID(id)) match {
58+
case Some(ds) => {
59+
children = getChildrenOfDataset(Some(ds), mine, user)
60+
}
61+
case None =>
62+
}
63+
}
64+
case None => children = getDatasets(mine, user)
65+
}
66+
} else if (nodeType == "folder"){
67+
nodeId match {
68+
case Some(id) => {
69+
folderService.get(UUID(id)) match {
70+
case Some(folder) => {
71+
children = getChildrenOfFolder(folder, mine, user)
72+
}
73+
case None =>
74+
}
75+
}
76+
case None =>
77+
}
78+
} else if (nodeType == "root"){
79+
children = getSpacesAndOrphanCollectionsDatasets(mine, user)
80+
}
81+
children
82+
}
83+
84+
def getChildrenOfSpace(space: Option[ProjectSpace], mine: Boolean, user : User): List[JsValue] = {
85+
var children : ListBuffer[JsValue] = ListBuffer.empty[JsValue]
86+
87+
val numCollectionsInSpace : Long = collections.countSpace(space.get.id.toString())
88+
var collectionsInSpace = spaceService.getCollectionsInSpace(Some(space.get.id.stringify),Some(numCollectionsInSpace.toInt))
89+
90+
val numDatasetsInSpace : Long = datasets.countSpace(space.get.id.stringify)
91+
var datasetsInSpace = spaceService.getDatasetsInSpace(Some(space.get.id.stringify),Some(numDatasetsInSpace.toInt))
92+
// filters datasets that have a collection in the space
93+
datasetsInSpace = datasetsInSpace.filter((d: Dataset) => (!datasetHasCollectionInSpace(d,space.get)))
94+
if (mine){
95+
collectionsInSpace = collectionsInSpace.filter((c: Collection) => (c.author.id == user.id))
96+
datasetsInSpace = datasetsInSpace.filter((d: Dataset) => (d.author.id == user.id))
97+
} else {
98+
99+
}
100+
for (d <- datasetsInSpace){
101+
var dsjson : JsValue = datasetJson(d)
102+
children += dsjson
103+
}
104+
for (c <- collectionsInSpace){
105+
var coljson : JsValue = collectionJson(c)
106+
children += coljson
107+
}
108+
children.toList
109+
}
110+
111+
def getChildrenOfCollection(collection: Option[Collection], mine: Boolean, user : User): List[JsValue] = {
112+
var children : ListBuffer[JsValue] = ListBuffer.empty[JsValue]
113+
var child_collections = collections.listChildCollections(collection.get.id)
114+
var datasets_in_collection = datasets.listCollection(collection.get.id.stringify, Some(user))
115+
116+
for (child <- child_collections){
117+
var childColJson : JsValue = collectionJson(child)
118+
children += childColJson
119+
}
120+
for (ds <- datasets_in_collection){
121+
var dsJson : JsValue = datasetJson(ds)
122+
children += dsJson
123+
}
124+
125+
children.toList
126+
}
127+
128+
def getOrphanCollectionsNotInSpace(user : User) : List[Collection] = {
129+
val numCollectionsUser = collections.countUser(Some(user), true, user)
130+
var collectionsNotInSpace = collections.listUser(numCollectionsUser.toInt, Some(user),false,user).filter((c: Collection) => (c.spaces.isEmpty && c.parent_collection_ids.isEmpty))
131+
collectionsNotInSpace
132+
}
133+
134+
def getOrphanDatasetsNotInSpace(user : User ) : List[Dataset] = {
135+
var numDatasetsUser = datasets.countUser(Some(user), true, user)
136+
var datasetsNotInSpace = datasets.listUser(numDatasetsUser.toInt, Some(user),false,user).filter((d: Dataset) => (d.spaces.isEmpty && d.collections.isEmpty))
137+
datasetsNotInSpace
138+
}
139+
140+
def getChildrenOfDataset(dataset: Option[Dataset], mine: Boolean, user : User): List[JsValue] = {
141+
var children : ListBuffer[JsValue] = ListBuffer.empty[JsValue]
142+
var ds_file_ids = dataset.get.files
143+
for (f <- ds_file_ids){
144+
fileService.get(f) match {
145+
case Some(file) => {
146+
var fjson : JsValue = fileJson(file)
147+
children += fjson
148+
}
149+
case None =>
150+
}
151+
}
152+
var ds_folders= folderService.findByParentDatasetId(dataset.get.id)
153+
for (ds_folder <- ds_folders){
154+
folderService.get(ds_folder.id) match {
155+
case Some(folder) => {
156+
if (folder.parentType == "dataset"){
157+
var fjson :JsValue = folderJson(folder)
158+
children += fjson
159+
}
160+
}
161+
case None =>
162+
}
163+
}
164+
children.toList
165+
}
166+
167+
def getChildrenOfFolder(folder: Folder, mine: Boolean, user: User) : List[JsValue] = {
168+
var children : ListBuffer[JsValue] = ListBuffer.empty[JsValue]
169+
170+
var subfolders_ids = folder.folders
171+
for (subfolder_id <- subfolders_ids){
172+
folderService.get(subfolder_id) match {
173+
case Some(subfolder) => {
174+
var sfjson = folderJson(subfolder)
175+
children += sfjson
176+
}
177+
case None =>
178+
}
179+
}
180+
181+
var file_ids = folder.files
182+
for (f <- file_ids){
183+
fileService.get(f) match {
184+
case Some(file) => {
185+
var fjson : JsValue = fileJson(file)
186+
children += fjson
187+
}
188+
case None =>
189+
}
190+
}
191+
192+
children.toList
193+
}
194+
195+
def getSpacesAndOrphanCollectionsDatasets(mine: Boolean, user : User) : List[JsValue] = {
196+
var root_level_nodes : ListBuffer[JsValue] = ListBuffer.empty[JsValue]
197+
var spaces = spaceService.listAccess(0,Set[Permission](Permission.ViewSpace),Some(user),true,true,false,false)
198+
if (mine){
199+
spaces = spaces.filter((s: ProjectSpace) => (s.creator == user.id))
200+
}
201+
for (s <- spaces){
202+
var s_json = spaceJson(s)
203+
root_level_nodes += s_json
204+
}
205+
var orphan_cols = getOrphanCollectionsNotInSpace(user)
206+
for (c <- orphan_cols){
207+
var c_json = collectionJson(c)
208+
root_level_nodes += c_json
209+
}
210+
211+
var orphan_ds = getOrphanDatasetsNotInSpace(user)
212+
for (d <- orphan_ds){
213+
var d_json = datasetJson(d)
214+
root_level_nodes += d_json
215+
}
216+
root_level_nodes.toList
217+
}
218+
219+
def getSpaces(mine : Boolean, user: User) : List[JsValue] = {
220+
var rootSpaces : ListBuffer[JsValue] = ListBuffer.empty[JsValue]
221+
var spaces = spaceService.listAccess(0,Set[Permission](Permission.ViewSpace),Some(user),true,true,false,false)
222+
if (mine){
223+
spaces = spaces.filter((s: ProjectSpace) => (s.creator == user.id))
224+
}
225+
for (s <- spaces){
226+
var s_json = spaceJson(s)
227+
rootSpaces += s_json
228+
}
229+
rootSpaces.toList
230+
}
231+
232+
def getDatasets(mine : Boolean, user: User) : List[JsValue] = {
233+
val numDatasetsUser = datasets.countUser(Some(user), true, user).toInt
234+
var visibleDatasets : ListBuffer[JsValue] = ListBuffer.empty[JsValue]
235+
var ds = datasets.listAccess(numDatasetsUser, Set[Permission](Permission.ViewDataset),Some(user),true,true,false)
236+
// ds = ds.filter((d: Dataset) => (d.trash == false))
237+
if (mine){
238+
ds = ds.filter((d: Dataset) => (d.author.id == user.id))
239+
}
240+
for (d <- ds){
241+
var d_json = datasetJson(d)
242+
visibleDatasets += d_json
243+
}
244+
visibleDatasets.toList
245+
}
246+
247+
def getCollections(mine: Boolean, user: User) : List[JsValue] = {
248+
val numCollectionsUser = collections.countUser(Some(user), true, user).toInt
249+
var cols = collections.listAccess(numCollectionsUser, Set[Permission](Permission.ViewCollection),Some(user),true,true,false)
250+
// cols = cols.filter((c : Collection) => (c.trash == false))
251+
if (mine) {
252+
cols = cols.filter((c: Collection) => (c.author.id == user.id))
253+
}
254+
var visibleCollection : ListBuffer[JsValue] = ListBuffer.empty[JsValue]
255+
for (col <-cols){
256+
visibleCollection += collectionJson(col)
257+
258+
}
259+
visibleCollection.toList
260+
}
261+
262+
private def collectionJson(collection: Collection) : JsValue = {
263+
var hasChildren = false
264+
if (collection.child_collection_ids.size > 0 || collection.datasetCount > 0){
265+
hasChildren = true
266+
}
267+
var data = Json.obj("type"->"collection")
268+
Json.obj("id" -> collection.id.toString, "name" -> collection.name, "text" -> collection.name,
269+
"authorId" -> collection.author.id, "children"->hasChildren,"type"->"collection", "data"->data, "icon"->"glyphicon glyphicon-th-large")
270+
}
271+
272+
private def datasetJson(dataset: Dataset) : JsValue = {
273+
var children = false;
274+
if (dataset.files.length > 0 || dataset.folders.length > 0) {
275+
children = true;
276+
}
277+
var data = Json.obj("type"->"dataset")
278+
Json.obj("id" -> dataset.id.toString, "name" -> dataset.name,"text"->dataset.name, "authorId" -> dataset.author.id,
279+
"spaces" -> dataset.spaces,"children"->children, "type"->"dataset","data"->data, "icon"->"glyphicon glyphicon-briefcase")
280+
281+
}
282+
283+
private def spaceJson(space: ProjectSpace) : JsValue = {
284+
var hasChildren = false
285+
if (space.datasetCount > 0 || space.collectionCount > 0){
286+
hasChildren = true
287+
}
288+
var data = Json.obj("type"->"space")
289+
Json.obj("id"-> space.id.toString, "name"->space.name ,"text"->space.name , "children"->hasChildren, "type"->"space","data"->data, "icon" -> "glyphicon glyphicon-hdd")
290+
}
291+
292+
private def fileJson(file: File) : JsValue = {
293+
var data = Json.obj("type"->"file")
294+
Json.obj("id"->file.id, "name"->file.filename, "text"->file.filename, "type"->"file","data"->data,"icon"-> "glyphicon glyphicon-file")
295+
}
296+
297+
private def folderJson(folder: Folder) : JsValue = {
298+
var children = false;
299+
if (folder.files.length > 0 || folder.folders.length > 0) {
300+
children = true;
301+
}
302+
303+
var data = Json.obj("type"->"folder","parentDataset"->folder.parentDatasetId)
304+
Json.obj("id"->folder.id, "name"->folder.name,"text"->folder.name, "data"->data, "children"->children)
305+
306+
}
307+
308+
private def datasetHasCollectionInSpace(dataset : Dataset, space : ProjectSpace) : Boolean = {
309+
var hasCollectionInSpace = false;
310+
var datasetCollectionIds = dataset.collections
311+
if (datasetCollectionIds.isEmpty){
312+
hasCollectionInSpace = false
313+
return hasCollectionInSpace
314+
}
315+
for (col_id <- datasetCollectionIds){
316+
collections.get(col_id) match {
317+
case Some(col) => {
318+
if (col.spaces.contains(space.id)){
319+
hasCollectionInSpace = true
320+
return hasCollectionInSpace
321+
}
322+
}
323+
}
324+
}
325+
return hasCollectionInSpace
326+
}
327+
328+
}

0 commit comments

Comments
 (0)