You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dlab.apache.org by dg...@apache.org on 2020/04/23 16:19:40 UTC

[incubator-dlab] branch bucket-browser-gcp updated: [DLAB-1551]: Merge UI in bucket browser (#701)

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

dgnatyshyn pushed a commit to branch bucket-browser-gcp
in repository https://gitbox.apache.org/repos/asf/incubator-dlab.git


The following commit(s) were added to refs/heads/bucket-browser-gcp by this push:
     new f45a0d7  [DLAB-1551]: Merge UI in bucket browser (#701)
f45a0d7 is described below

commit f45a0d78fdc78d573d626d72192d6d7f22f28b87
Author: Dmytro Gnatyshyn <42...@users.noreply.github.com>
AuthorDate: Thu Apr 23 19:19:33 2020 +0300

    [DLAB-1551]: Merge UI in bucket browser (#701)
    
    [DLAB-1551]: Bucket browser UI
---
 .../core/interceptors/http.token.interceptor.ts    |   2 +-
 .../services/applicationServiceFacade.service.ts   |  29 +++
 .../app/core/services/bucket-browser.service.ts    | 153 +++++++++++++
 .../bucket-browser/bucket-browser.component.html   |  90 ++++++++
 .../bucket-browser/bucket-browser.component.scss   | 243 +++++++++++++++++++++
 .../bucket-browser/bucket-browser.component.ts     | 205 +++++++++++++++++
 .../folder-tree/folder-tree.component.html         |  32 +++
 .../folder-tree/folder-tree.component.scss         |  38 ++++
 .../folder-tree/folder-tree.component.ts           | 191 ++++++++++++++++
 .../detail-dialog/detail-dialog.component.html     |   4 +-
 .../detail-dialog/detail-dialog.component.scss     |   1 +
 .../detail-dialog/detail-dialog.component.ts       |  11 +-
 .../src/app/resources/resources.component.html     |   3 +
 .../src/app/resources/resources.component.ts       |   6 +
 .../webapp/src/app/resources/resources.module.ts   |  12 +-
 .../webapp/src/assets/styles/_dialogs.scss         |   1 +
 16 files changed, 1013 insertions(+), 8 deletions(-)

diff --git a/services/self-service/src/main/resources/webapp/src/app/core/interceptors/http.token.interceptor.ts b/services/self-service/src/main/resources/webapp/src/app/core/interceptors/http.token.interceptor.ts
index ce476b4..29aa010 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/interceptors/http.token.interceptor.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/interceptors/http.token.interceptor.ts
@@ -33,7 +33,7 @@ import { Observable } from 'rxjs';
     if (token)
       headersConfig['Authorization'] = `Bearer ${token}`;
 
-    if (!request.headers.has('Content-Type') && !request.headers.has('Upload'))
+    if (!request.headers.has('Content-Type') && !request.headers.has('Upload') && request.url.indexOf('upload') === -1)
       headersConfig['Content-Type'] = 'application/json; charset=UTF-8';
 
     const header = request.clone({ setHeaders: headersConfig });
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts
index 121188e..ebaec6d 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts
@@ -52,6 +52,7 @@ export class ApplicationServiceFacade {
   private static readonly COMPUTATIONAL_RESOURCES = 'computational_resources';
   private static readonly COMPUTATIONAL_RESOURCES_DATAENGINE = 'computational_resources_dataengine';
   private static readonly COMPUTATIONAL_RESOURCES_DATAENGINESERVICE = 'computational_resources_dataengineservice';
+  private static readonly BUCKET = 'bucket';
   private static readonly USER_PREFERENCES = 'user_preferences';
   private static readonly BUDGET = 'budget';
   private static readonly ENVIRONMENT_HEALTH_STATUS = 'environment_health_status';
@@ -253,6 +254,31 @@ export class ApplicationServiceFacade {
       null);
   }
 
+  public buildGetBucketData(): Observable<any> {
+    return this.buildRequest(HTTPMethod.GET,
+      this.requestRegistry.Item(ApplicationServiceFacade.BUCKET) + '/ofuks-1304-prj1-local-bucket/endpoint/local',
+     null);
+  }
+
+  public buildUploadFileToBucket(data): Observable<any> {
+    return this.buildRequest(HTTPMethod.POST,
+      this.requestRegistry.Item(ApplicationServiceFacade.BUCKET) + '/upload',
+      data);
+  }
+
+  public buildDownloadFileFromBucket(data): Observable<any> {
+    return this.buildRequest(HTTPMethod.GET,
+      this.requestRegistry.Item(ApplicationServiceFacade.BUCKET),
+      data, { observe: 'response', responseType: 'text' } );
+  }
+
+  public buildDeleteFileFromBucket(data): Observable<any> {
+    return this.buildRequest(HTTPMethod.DELETE,
+      this.requestRegistry.Item(ApplicationServiceFacade.BUCKET),
+      data );
+  }
+
+
   public buildUpdateUserPreferences(data): Observable<any> {
     return this.buildRequest(HTTPMethod.POST,
       this.requestRegistry.Item(ApplicationServiceFacade.USER_PREFERENCES),
@@ -651,6 +677,9 @@ export class ApplicationServiceFacade {
     this.requestRegistry.Add(ApplicationServiceFacade.COMPUTATIONAL_RESOURCES_TEMLATES,
       '/api/infrastructure_templates/computational_templates');
 
+    // Bucket browser
+    this.requestRegistry.Add(ApplicationServiceFacade.BUCKET, '/api/bucket');
+
     // Filtering Configuration
     this.requestRegistry.Add(ApplicationServiceFacade.USER_PREFERENCES, '/api/user/settings');
     this.requestRegistry.Add(ApplicationServiceFacade.BUDGET, '/api/user/settings/budget');
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/bucket-browser.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/bucket-browser.service.ts
new file mode 100644
index 0000000..ff27ef5
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/bucket-browser.service.ts
@@ -0,0 +1,153 @@
+import { Injectable } from '@angular/core';
+import {BehaviorSubject, Observable} from 'rxjs';
+import {catchError, map} from 'rxjs/operators';
+import {ErrorUtils} from '../util';
+import {ApplicationServiceFacade} from './applicationServiceFacade.service';
+
+export class TodoItemNode {
+  children: TodoItemNode[];
+  item: string;
+  id: string;
+  size: number;
+}
+
+/** Flat to-do item node with expandable and level information */
+export class TodoItemFlatNode {
+  item: string;
+  level: number;
+  expandable: boolean;
+}
+
+/**
+ * The Json object for to-do list data.
+ */
+
+
+const array = [{'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '4.txt', 'size': '18 bytes', 'creationDate': '21-4-2020 11:36:36'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '5.txt', 'size': '18 bytes', 'creationDate': '21-4-2020 11:56:46'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'Untitled', 'size': '5 bytes', 'creationDate': '13-4-2020 03:39:11'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'adasdas', 'size': '1 KB', 'creationDate': '15-4-2020 02:17 [...]
+
+const processFiles = (files, target) => {
+  let pointer = target;
+  files.forEach((file, index) => {
+    if (!pointer[file]) {
+      pointer[file] = {};
+    }
+    pointer = pointer[file];
+  });
+
+};
+
+const processFolderArray = (acc, curr) => {
+  const files = curr.object.split('/').filter(x => x.length > 0);
+  processFiles(files, acc);
+  return acc;
+};
+
+const convertToFolderTree = (data) => data
+  .reduce(
+    processFolderArray,
+    {}
+  );
+
+const TREE_DATA = convertToFolderTree(array);
+
+
+@Injectable({
+  providedIn: 'root'
+})
+export class BucketBrowserService {
+  public dataChange = new BehaviorSubject<TodoItemNode[]>([]);
+  public serverData: any = [];
+  get data(): TodoItemNode[] { return this.dataChange.value; }
+
+  constructor(private applicationServiceFacade: ApplicationServiceFacade) {
+    this.initialize();
+  }
+
+  public getBacketData(): Observable<{}> {
+    return this.applicationServiceFacade
+      .buildGetBucketData()
+      .pipe(
+        map(response => response),
+        catchError(ErrorUtils.handleServiceError));
+  }
+
+  public initialize() {
+    let backetData = [];
+    this.getBacketData().subscribe(v => {
+      this.serverData = v;
+      backetData = convertToFolderTree(v);
+      const data = this.buildFileTree({'ofuks-1304-prj1-local-bucket': backetData}, 0);
+      this.dataChange.next(data);
+    });
+    // this.serverData = array;
+    // backetData = convertToFolderTree(this.serverData);
+    // const data = this.buildFileTree({'ofuks-1304-prj1-local-bucket': backetData}, 0);
+    // this.dataChange.next(data);
+  }
+
+  /**
+   * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
+   * The return value is the list of `TodoItemNode`.
+   */
+  public buildFileTree(obj: {[key: string]: any}, level: number): TodoItemNode[] {
+    return Object.keys(obj).reduce<TodoItemNode[]>((accumulator, key) => {
+      const value = obj[key];
+      const node = new TodoItemNode();
+      node.item = key;
+      if (Object.keys(value).length) {
+        if (typeof value === 'object') {
+          node.children = this.buildFileTree(value, level + 1);
+        } else {
+          node.item = value;
+        }
+      } else {
+        node.size = this.serverData.filter(v => v.object.indexOf(node.item) !== -1)[0];
+      }
+      return accumulator.concat(node);
+    }, []);
+  }
+
+  public insertItem(parent: TodoItemNode, name, isFile) {
+    if (parent.children) {
+      if (isFile) {
+        parent.children.push(name as TodoItemNode);
+      } else {
+        parent.children.unshift({item: name, children: []} as TodoItemNode);
+        this.dataChange.next(this.data);
+      }
+    }
+  }
+
+  public updateItem(node: TodoItemNode, file) {
+    node.item = file;
+    this.dataChange.next(this.data);
+  }
+
+  public downloadFile(data) {
+    return this.applicationServiceFacade
+      .buildDownloadFileFromBucket(data)
+      .pipe(
+        map(response => response),
+        catchError(ErrorUtils.handleServiceError));
+  }
+
+  public uploadFile(data) {
+    return this.applicationServiceFacade
+      .buildUploadFileToBucket(data)
+      .pipe(
+        map(response => response),
+        catchError(ErrorUtils.handleServiceError));
+  }
+
+  public deleteFile(data) {
+    const url = JSON.stringify(data)
+    return this.applicationServiceFacade
+      .buildDeleteFileFromBucket(url)
+      .pipe(
+        map(response => response),
+        catchError(ErrorUtils.handleServiceError));
+  }
+  // initBucket(bucketType) {
+  //   bucketType !== 'project' ? TREE_DATA = local : TREE_DATA = projecta;
+  // }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html
new file mode 100644
index 0000000..8671562
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html
@@ -0,0 +1,90 @@
+<!--
+  ~ 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="bucket-browser" id="dialog-box">
+  <header class="dialog-header">
+    <h4 class="modal-title">Bucket browser</h4>
+    <button type="button" class="close" (click)="dialogRef.close()">&times;</button>
+  </header>
+  <div class="dialog-content tabs">
+    <div class="submit m-bott-10 m-top-10">
+      <button mat-raised-button type="button" class="butt action"><input type="file" (change)="handleFileInput($event)">Add file</button>
+      <button mat-raised-button type="button" class="butt action" (click)="folderTreeComponent.addNewItem(selectedFolder, '', false)">Create folder</button>
+    </div>
+    <p class="path"><span>Bucket path:</span><span class="url"> {{path}}</span></p>
+    <div class="backet-wrapper" id="scrolling">
+      <div class="navigation">
+        <dlab-folder-tree (showFolderContent)=onFolderClick($event)> </dlab-folder-tree>
+      </div>
+      <div class="directory">
+        <ul class="folder-tree" *ngIf="!addedFiles.length">
+          <li *ngFor="let file of folderItems" class="folder-item" >
+
+            <div class="folder-item-wrapper" *ngIf="file.children" (click)="showItem(file)">
+              <div class="name name-folder"><i class="material-icons folder-icon" >folder</i><span>{{file.item}}</span></div>
+              <div class="size"></div>
+              <div class="progress-wrapper">
+                <div class="progres" *ngIf="file.isSelected"><div class="bar"></div></div>
+              </div>
+            </div>
+
+            <div class="folder-item-wrapper"  (click)="toggleSelectedFile(file)" *ngIf="!file.children">
+              <div class="name name-file">
+                 <span class="empty-checkbox" [ngClass]="{'checked': file.isSelected}" (click)="toggleSelectedFile(file);$event.stopPropagation()" >
+                    <span class="checked-checkbox" *ngIf="file.isSelected"></span>
+                  </span>
+                <span class="item-name">{{file.item}}</span>
+              </div>
+              <div class="size">{{file.size.size}}</div>
+              <div class="size">{{file.size.creationDate }}</div>
+<!--              <div class="progress-wrapper">-->
+<!--&lt;!&ndash;                <div class="progres" *ngIf="file.isSelected"><div class="bar"></div></div>&ndash;&gt;-->
+<!--              </div>-->
+            </div>
+
+          </li>
+        </ul>
+        <ul class="folder-tree">
+          <li *ngFor="let file of addedFiles" class="folder-item">
+            <div class="folder-item-wrapper">
+              <div class="name">{{file.item}}</div>
+              <div class="size">{{file.size}} MB</div>
+              <div class="progress-wrapper">
+<!--                <div class="progres">-->
+<!--                  <div class="bar" [ngClass]="{'full': isUploading}">-->
+<!--                    -->
+<!--                  </div>-->
+
+<!--                </div>-->
+              <mat-progress-bar mode="indeterminate" *ngIf="isUploading"></mat-progress-bar>
+              </div>
+              <div (click)="deleteAddedFile(file)"><i class="material-icons close">close</i></div>
+            </div>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <div class="text-center m-top-30 m-bott-30">
+      <button type="button" class="butt" mat-raised-button (click)="this.dialogRef.close()">Close</button>
+      <button *ngIf="!this.addedFiles.length" type="button" class="butt butt-success" mat-raised-button (click)="fileAction('download')" [disabled]="!selected?.length">Download</button>
+      <button *ngIf="!this.addedFiles.length" type="button" class="butt butt-success" mat-raised-button (click)="fileAction('delete')" [disabled]="!selected?.length">Delete</button>
+      <button *ngIf="this.addedFiles.length !== 0" type="button" class="butt butt-success" mat-raised-button (click)="uploadNewFile()">Upload</button>
+    </div>
+  </div>
+</div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss
new file mode 100644
index 0000000..b9033da
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss
@@ -0,0 +1,243 @@
+/*!
+ * 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.
+ */
+
+.bucket-browser {
+  .path{
+    margin: 0 4px 10px 4px;
+    padding: 4px 4px 4px 20px;
+    color: rgba(0,0,0,.87);
+    .url{
+      font-weight: 600;
+    }
+  }
+  bottom: 0;
+  .dialog-content{
+    padding: 0 35px;
+  }
+  .content-box {
+    height: 500px;
+
+  }
+
+  .submit{
+    display: flex;
+    .butt{
+      position: relative;
+      overflow: hidden;
+      margin: 10px;
+      input[type="file"] {
+        position: absolute;
+        left: 0;
+        right: 0;
+        top: 0;
+        bottom: 0;
+        opacity: 0;
+      }
+    }
+  }
+
+  .text-center{
+    position: absolute;
+    bottom: 3vh;
+    left: 0;
+    right: 0;
+  }
+}
+
+.backet-wrapper{
+  height: 50vh;
+  border: 2px solid rgba(0,0,0,.12);
+  border-radius: 5px;
+  display: flex;
+
+
+
+  .navigation{
+    flex: 1;
+    border-right: 2px solid rgba(0,0,0,.12);
+    max-height: 500px;
+    overflow: auto;
+    .folder-tree{
+
+      .folder{
+        line-height: 30px;
+      }
+    }
+
+  }
+
+  .directory{
+    max-height: 500px;
+    overflow: auto;
+    flex: 2;
+    .folder-tree{
+      .name{
+        flex:2;
+        display: flex;
+        align-items: center;
+        &-folder{
+          span{
+            padding-left: 10px;
+          }
+        }
+        &-file{
+          padding-left: 4px;
+          span.item-name{
+            padding-left: 14px;
+          }
+        }
+      }
+      .size{
+        flex:1;
+      }
+      .progress-wrapper{
+        flex:1;
+        .progres{
+          border: 1px solid rgba(0,0,0,.12);
+          height: 15px;
+          position: relative;
+          .bar{
+            position: absolute;
+            top: 0;
+            bottom: 0;
+            left: 0;
+            width: 0;
+            background-color:  #00bcd4;
+            &.full{
+              width: 100%;
+              transition: 5s ease-in-out;
+            }
+          }
+        }
+      }
+      .material-icons.close{
+        font-size: 15px;
+        margin: 0 10px;
+        cursor: pointer;
+      }
+      .action {
+        display: flex;
+        justify-content: space-around;
+        width: 100%;
+        .add-file {
+          overflow: hidden;
+          min-width: 100px ;
+          height: 30px;
+          position: relative;
+          input{
+            position: absolute;
+            left: 0;
+            right: 0;
+            top: 0;
+            bottom: 0;
+            opacity: 0;
+          }
+        }
+      }
+
+    }
+  }
+
+  .folder-tree {
+    padding: 20px 30px;
+  }
+
+  .folder-item{
+     display: flex;
+     align-items: center;
+     .folder-item-wrapper{
+       width: 100%;
+       display: flex;
+       justify-content: space-between;
+       align-items: center;
+       cursor: pointer;
+       color: rgba(0,0,0,.87);
+       i{
+         color: rgb(232, 232, 232);
+       }
+       &:hover{
+         color: #00bcd4;
+         transition: .3s ease-in-out;
+         i{
+           color: #00bcd4;
+           transition: .3s ease-in-out;
+         }
+         .empty-checkbox{
+           border-color: #00bcd4
+         }
+         .progress-wrapper{
+           .progres{
+             border-color: #00bcd4 !important;
+           }
+         }
+       }
+     }
+   }
+}
+
+.empty-checkbox {
+  width: 16px;
+  height: 16px;
+  border-radius: 2px;
+  border: 2px solid lightgrey;
+  margin-top: 2px;
+  position: relative;
+  &.checked {
+    border-color: #35afd5;
+    background-color: #35afd5;
+  }
+  .checked-checkbox {
+    top: 0px;
+    left: 4px;
+    width: 5px;
+    height: 10px;
+    border-bottom: 2px solid white;
+    border-right: 2px solid white;
+    position: absolute;
+    transform: rotate(45deg);
+  }
+}
+
+.folder-item-name{
+  span{
+    color: #000;
+  }
+
+  .check-box {
+    background-color: red;
+  }
+
+}
+
+@media only screen and (max-height: 840px) {
+  .backet-wrapper {
+    height: 45vh;
+  }
+}
+
+@media only screen and (max-height: 690px) {
+  .backet-wrapper {
+    height: 40vh;
+  }
+}
+
+
+
+
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts
new file mode 100644
index 0000000..fef7d0d
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts
@@ -0,0 +1,205 @@
+/*
+ * 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, OnInit, ViewChild, Inject } from '@angular/core';
+import { FormGroup, FormBuilder, Validators } from '@angular/forms';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { ToastrService } from 'ngx-toastr';
+import { ManageUngitService } from '../../core/services';
+
+import {FolderTreeComponent} from './folder-tree/folder-tree.component';
+import {BucketBrowserService, TodoItemNode} from '../../core/services/bucket-browser.service';
+import {FileUtils} from '../../core/util';
+
+@Component({
+  selector: 'dlab-bucket-browser',
+  templateUrl: './bucket-browser.component.html',
+  styleUrls: ['./bucket-browser.component.scss']
+})
+export class BucketBrowserComponent implements OnInit {
+  public filenames: Array<any> = [];
+  public addedFiles = [];
+  public folderItems = [];
+  public path = '';
+  public pathInsideBucket = '';
+  public bucketName = '';
+  public endpoint = '';
+
+  @ViewChild(FolderTreeComponent, {static: true}) folderTreeComponent;
+  public selectedFolder: any;
+  private isUploading: boolean;
+  private selected: any[];
+  private uploadForm: FormGroup;
+
+  constructor(
+    @Inject(MAT_DIALOG_DATA) public data: any,
+    public toastr: ToastrService,
+    public dialog: MatDialog,
+    public dialogRef: MatDialogRef<BucketBrowserComponent>,
+    private manageUngitService: ManageUngitService,
+    private _fb: FormBuilder,
+    private bucketBrowserService: BucketBrowserService,
+    private formBuilder: FormBuilder
+  ) {
+
+  }
+
+  ngOnInit() {
+    // this.bucketBrowserService.getBacketData();
+    this.endpoint = this.data.endpoint;
+    this.uploadForm = this.formBuilder.group({
+      file: ['']
+    });
+  }
+
+  public showItem(item) {
+    const flatItem = this.folderTreeComponent.nestedNodeMap.get(item);
+    this.folderTreeComponent.showItem(flatItem);
+  }
+
+  public handleFileInput(event) {
+    //   for (let i = 0; i < files.length; i++) {
+    //     const file = files[i];
+    //     const path = file.webkitRelativePath.split('/');
+    //   }
+    // }
+    if (event.target.files.length > 0) {
+      const file = event.target.files[0];
+      this.uploadForm.get('file').setValue(file);
+      this.filenames = Object['values'](event.target.files).map(v => ({item: v.name, 'size': (v.size / 1048576).toFixed(2)} as unknown as TodoItemNode));
+      this.addedFiles = [...this.addedFiles, ...this.filenames];
+    }
+
+  }
+
+  public toggleSelectedFile(file) {
+    file.isSelected = !file.isSelected;
+    this.selected = this.folderItems.filter(item => item.isSelected);
+  }
+
+  filesPicked(files) {
+    // console.log(files);
+
+    Array.prototype.forEach.call(files, file => {
+      this.addedFiles.push(file.webkitRelativePath);
+    });
+    // console.log(this.addedFiles);
+  }
+
+  public onFolderClick(event) {
+    this.selectedFolder = event.flatNode;
+    this.folderItems = event.element ? event.element.children : event.children;
+    // this.folderItems = this.folderItems.sort((a, b) => (a.children > b.children) ? 1 : -1)
+    // console.log(this.folderItems);
+    this.path = event.path;
+    this.pathInsideBucket = this.path.indexOf('/') !== -1 ?  this.path.slice(this.path.indexOf('/') + 1) + '/' : '';
+    this.bucketName = this.path.substring(0, this.path.indexOf('/')) || this.path;
+    this.folderItems.forEach(item => item.isSelected = false);
+  }
+
+  private upload(tree, path) {
+    tree.files.forEach(file => {
+      this.addedFiles.push(path + file.name);
+    });
+    tree.directories.forEach(directory => {
+      const newPath = path + directory.name + '/';
+      this.addedFiles.push(newPath);
+      this.upload(directory, newPath);
+    });
+  }
+
+  public deleteAddedFile(file) {
+    this.addedFiles.splice(this.addedFiles.indexOf(file), 1);
+  }
+
+  private uploadNewFile() {
+    const path = `${this.pathInsideBucket}${this.uploadForm.get('file').value.name}`;
+
+    const formData = new FormData();
+    formData.append('file', this.uploadForm.get('file').value);
+    formData.append('object', path);
+    formData.append('bucket', 'ofuks-1304-prj1-local-bucket');
+    formData.append('endpoint', this.endpoint);
+    // file.inProgress = true;
+    this.isUploading = true;
+    this.bucketBrowserService.uploadFile(formData)
+      // .pipe(
+      // map(event => {
+      //   switch (event.type) {
+      //     case HttpEventType.UploadProgress:
+      //       file.progress = Math.round(event.loaded * 100 / event.total);
+      //       break;
+      //     case HttpEventType.Response:
+      //       return event;
+      //   }
+      // }),
+      // catchError((error: HttpErrorResponse) => {
+      //   file.inProgress = false;
+      //   return of(`${file.name} upload failed.`);
+      // }))
+      .subscribe((event: any) => {
+      //   if (typeof (event) === 'object') {
+      //     console.log(event.body);
+      //   }
+      // this.isUploading = false;
+        this.bucketBrowserService.initialize();
+        this.addedFiles = [];
+        this.isUploading = false;
+        this.toastr.success('File successfully uploaded!', 'Success!');
+      }, error => this.toastr.error(error.message || 'File uploading error!', 'Oops!')
+    );
+  }
+
+  public fileAction(action) {
+    this.selected = this.folderItems.filter(item => item.isSelected);
+    const path = encodeURIComponent(`${this.pathInsideBucket}${this.selected[0].item}`);
+    if (action === 'download') {
+      this.bucketBrowserService.downloadFile(`/${this.bucketName}/object/${path}/endpoint/${this.endpoint}/download`
+      ).subscribe(data =>  {
+        FileUtils.downloadFile(data);
+        // this.downLoadFile(response, 'aplication/octet-stream');
+          this.toastr.success('File downloading started!', 'Success!');
+        }, error => this.toastr.error(error.message || 'File downloading error!', 'Oops!')
+      );
+    }
+
+    if (action === 'delete') {
+      this.bucketBrowserService.deleteFile(`/${this.bucketName}/object/${path}/endpoint/${this.endpoint}`).subscribe(() => {
+        this.bucketBrowserService.initialize();
+          this.toastr.success('File successfully deleted!', 'Success!');
+        }, error => this.toastr.error(error.message || 'File deleting error!', 'Oops!')
+      );
+    }
+
+    this.folderItems.forEach(item => item.isSelected = false);
+    this.selected = this.folderItems.filter(item => item.isSelected);
+  }
+
+  // downLoadFile(data: any, type: string) {
+  //   const blob = new Blob([data], { type: type});
+  //   const url = window.URL.createObjectURL(blob);
+  //   const pwa = window.open(url);
+  //   if (!pwa || pwa.closed || typeof pwa.closed === 'undefined') {
+  //     alert( 'Please disable your Pop-up blocker and try again.');
+  //   }
+  // }
+}
+
+
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html
new file mode 100644
index 0000000..b663e78
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html
@@ -0,0 +1,32 @@
+<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
+  <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding [ngStyle]="{'display': 'none'}">
+  <button mat-icon-button disabled></button>
+  <!--    <mat-checkbox class="checklist-leaf-node"-->
+  <!--                  [checked]="checklistSelection.isSelected(node)"-->
+  <!--                  (change)="todoLeafItemSelectionToggle(node)"></mat-checkbox>-->
+  {{node.item}}
+</mat-tree-node>
+
+  <mat-tree-node *matTreeNodeDef="let node; when: hasNoContent" matTreeNodePadding>
+    <button mat-icon-button disabled></button>
+    <mat-form-field>
+      <mat-label>New folder</mat-label>
+      <input matInput #itemValue>
+    </mat-form-field>
+    <button mat-button (click)="saveNode(node, itemValue.value)">Save</button>
+  </mat-tree-node>
+
+  <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
+    <button mat-icon-button matTreeNodeToggle
+            [attr.aria-label]="'toggle ' + node.filename">
+      <mat-icon class="mat-icon-rtl-mirror" [ngClass]="{'active-item': selectedFolder === node}">
+        {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
+      </mat-icon>
+    </button>
+<!--    <mat-checkbox [checked]="descendantsAllSelected(node)"-->
+<!--                  [indeterminate]="descendantsPartiallySelected(node)"-->
+<!--                  (change)="todoItemSelectionToggle(node)"></mat-checkbox>-->
+    <div (click)="showItem(node)" class="folder-item-line"  [ngClass]="{'active-item': selectedFolder === node}"><i class="material-icons folder-icon">folder</i> <span class="folder">{{node.item}}</span></div>
+<!--    <button mat-icon-button (click)="addNewItem(node)"><mat-icon>add</mat-icon></button>-->
+  </mat-tree-node>
+</mat-tree>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss
new file mode 100644
index 0000000..e9902af
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss
@@ -0,0 +1,38 @@
+
+.folder{
+  padding-left: 5px;
+}
+
+.folder-icon{
+  color: rgb(232, 232, 232);
+}
+
+.folder-item-line{
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+
+}
+
+.active-item {
+  //border-bottom: 1px solid #00bcd4;
+  color: #00bcd4;
+  i{
+    color: #00bcd4;
+  }
+
+
+}
+
+.mat-tree-node{
+  cursor: pointer;
+  transition: .3s;
+  overflow: unset;
+  &:hover{
+    color: #00bcd4;
+    i{
+      color: #00bcd4;
+    }
+  }
+}
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts
new file mode 100644
index 0000000..0b367c5
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts
@@ -0,0 +1,191 @@
+import {Component, OnInit, AfterViewInit, Output, EventEmitter} from '@angular/core';
+import {SelectionModel} from '@angular/cdk/collections';
+import {FlatTreeControl} from '@angular/cdk/tree';
+import {MatTreeFlatDataSource, MatTreeFlattener} from '@angular/material/tree';
+import {BucketBrowserService, TodoItemFlatNode, TodoItemNode} from '../../../core/services/bucket-browser.service';
+
+
+@Component({
+  selector: 'dlab-folder-tree',
+  templateUrl: './folder-tree.component.html',
+  styleUrls: ['./folder-tree.component.scss']
+})
+export class FolderTreeComponent implements OnInit {
+
+  @Output() showFolderContent: EventEmitter<any> = new EventEmitter();
+
+  path = [];
+  selectedFolder: TodoItemFlatNode;
+  flatNodeMap = new Map<TodoItemFlatNode, TodoItemNode>();
+  nestedNodeMap = new Map<TodoItemNode, TodoItemFlatNode>();
+  selectedParent: TodoItemFlatNode | null = null;
+  newItemName = '';
+  treeControl: FlatTreeControl<TodoItemFlatNode>;
+  treeFlattener: MatTreeFlattener<TodoItemNode, TodoItemFlatNode>;
+  dataSource: MatTreeFlatDataSource<TodoItemNode, TodoItemFlatNode>;
+
+  checklistSelection = new SelectionModel<TodoItemFlatNode>(true /* multiple */);
+
+  constructor(private bucketBrowserService: BucketBrowserService) {
+    this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel,
+      this.isExpandable, this.getChildren);
+
+    this.treeControl = new FlatTreeControl<TodoItemFlatNode>(this.getLevel, this.isExpandable);
+    this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
+
+    bucketBrowserService.dataChange.subscribe(data => {
+      this.dataSource.data = data;
+      if (!this.selectedFolder) {
+        const subject = this.dataSource._flattenedData;
+        subject.subscribe((subjectData) => {
+          this.treeControl.expand(subjectData[0]);
+          this.showItem(subjectData[0]);
+        });
+      }
+    });
+  }
+
+  getLevel = (node: TodoItemFlatNode) => node.level;
+
+  isExpandable = (node: TodoItemFlatNode) => node.expandable;
+
+  getChildren = (node: TodoItemNode): TodoItemNode[] => node.children;
+
+  hasChild = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.expandable;
+
+  hasNoContent = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.item === '';
+
+  transformer = (node: TodoItemNode, level: number) => {
+    const existingNode = this.nestedNodeMap.get(node);
+    const flatNode = existingNode && existingNode.item === node.item
+      ? existingNode
+      : new TodoItemFlatNode();
+    flatNode.item = node.item;
+    flatNode.level = level;
+    flatNode.expandable = !!node.children;
+    this.flatNodeMap.set(flatNode, node);
+    this.nestedNodeMap.set(node, flatNode);
+    return flatNode;
+  }
+
+
+  ngOnInit() {
+    // const subject = this.dataSource._flattenedData;
+    // subject.subscribe((data) => {
+    //   this.treeControl.expand(data[0]);
+    //   this.showItem(data[0]);
+    // });
+  }
+
+  showItem(el) {
+    if (el) {
+      this.treeControl.expand(el);
+      this.selectedFolder = el;
+      const path = this.getPath(el);
+      this.path = [];
+      const data = {
+        flatNode: el,
+        element: this.flatNodeMap.get(el),
+        path: path.join('/')
+      };
+      this.showFolderContent.emit(data);
+    }
+  }
+
+  getPath(el) {
+    if (el) {
+      if (this.path.length === 0) {
+        this.path.unshift(el.item);
+      }
+      if (this.getParentNode(el) !== null) {
+        this.path.unshift(this.getParentNode(el).item);
+        this.getPath(this.getParentNode(el));
+      }
+      return this.path;
+    }
+  }
+
+  descendantsAllSelected(node: TodoItemFlatNode): boolean {
+    const descendants = this.treeControl.getDescendants(node);
+    const descAllSelected = descendants.every(child =>
+      this.checklistSelection.isSelected(child)
+    );
+    return descAllSelected;
+  }
+
+  descendantsPartiallySelected(node: TodoItemFlatNode): boolean {
+    const descendants = this.treeControl.getDescendants(node);
+    const result = descendants.some(child => this.checklistSelection.isSelected(child));
+    return result && !this.descendantsAllSelected(node);
+  }
+
+  todoItemSelectionToggle(node: TodoItemFlatNode): void {
+    this.checklistSelection.toggle(node);
+  const descendants = this.treeControl.getDescendants(node);
+  this.checklistSelection.isSelected(node)
+? this.checklistSelection.select(...descendants)
+    : this.checklistSelection.deselect(...descendants);
+
+  // Force update for the parent
+  descendants.every(child =>
+  this.checklistSelection.isSelected(child)
+);
+  this.checkAllParentsSelection(node);
+}
+
+  todoLeafItemSelectionToggle(node: TodoItemFlatNode): void {
+    this.checklistSelection.toggle(node);
+    this.checkAllParentsSelection(node);
+  }
+
+  checkAllParentsSelection(node: TodoItemFlatNode): void {
+    let parent: TodoItemFlatNode | null = this.getParentNode(node);
+    while (parent) {
+      this.checkRootNodeSelection(parent);
+      parent = this.getParentNode(parent);
+    }
+  }
+
+  checkRootNodeSelection(node: TodoItemFlatNode): void {
+    const nodeSelected = this.checklistSelection.isSelected(node);
+    const descendants = this.treeControl.getDescendants(node);
+    const descAllSelected = descendants.every(child =>
+      this.checklistSelection.isSelected(child)
+    );
+    if (nodeSelected && !descAllSelected) {
+      this.checklistSelection.deselect(node);
+    } else if (!nodeSelected && descAllSelected) {
+      this.checklistSelection.select(node);
+    }
+  }
+
+  getParentNode(node: TodoItemFlatNode): TodoItemFlatNode | null {
+    const currentLevel = this.getLevel(node);
+
+    if (currentLevel < 1) {
+      return null;
+    }
+
+    const startIndex = this.treeControl.dataNodes.indexOf(node) - 1;
+
+    for (let i = startIndex; i >= 0; i--) {
+      const currentNode = this.treeControl.dataNodes[i];
+
+      if (this.getLevel(currentNode) < currentLevel) {
+        return currentNode;
+      }
+    }
+    return null;
+  }
+
+  addNewItem(node: TodoItemFlatNode, file, isFile, path) {
+    const parentNode = this.flatNodeMap.get(node);
+    this.bucketBrowserService.insertItem(parentNode!, file, isFile);
+    this.treeControl.expand(node);
+  }
+
+  saveNode(node: TodoItemFlatNode, itemValue: string) {
+    const nestedNode = this.flatNodeMap.get(node);
+    this.bucketBrowserService.updateItem(nestedNode!, itemValue);
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html
index 4977b22..817ca22 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html
@@ -60,14 +60,14 @@
                 class="strong">{{ notebook.password }}</span></p>
 
             <p class="m-top-30">{{ DICTIONARY[PROVIDER].personal_storage }}: &nbsp;</p>
-            <div class="links_block">
+            <div class="links_block" (click)="bucketBrowser('project', notebook.endpoint)">
               <p *ngIf="DICTIONARY[PROVIDER].cloud_provider === 'azure' && notebook.account_name">{{ DICTIONARY[PROVIDER].account }}
                 <span class="bucket-info">{{ notebook.account_name}}</span></p>
               <p *ngIf="notebook.bucket_name">{{ DICTIONARY[PROVIDER].container }} <span
                   class="bucket-info">{{ notebook.bucket_name }}</span></p>
             </div>
             <p>Shared endpoint bucket: &nbsp;</p>
-            <div class="links_block">
+            <div class="links_block" (click)="bucketBrowser('endpoint', notebook.endpoint)">
               <p *ngIf="DICTIONARY[PROVIDER].cloud_provider === 'azure' && notebook.shared_account_name">{{ DICTIONARY[PROVIDER].account }}
                 <span class="bucket-info">{{ notebook.shared_account_name}}</span></p>
               <p *ngIf="notebook.shared_bucket_name">{{ DICTIONARY[PROVIDER].container }}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss
index 2849ce0..12be81b 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss
@@ -86,4 +86,5 @@
   padding-left: 7px;
   font-weight: 600;
   color: $blue-grey-color;
+  cursor: pointer;
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts
index db097f3..ca264b9 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts
@@ -20,12 +20,13 @@
 import { Component, ViewChild, OnInit, Inject } from '@angular/core';
 import { FormGroup, FormBuilder } from '@angular/forms';
 import { ToastrService } from 'ngx-toastr';
-import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import {MatDialogRef, MAT_DIALOG_DATA, MatDialog} from '@angular/material/dialog';
 
 import { DateUtils, CheckUtils } from '../../../core/util';
 import { DICTIONARY } from '../../../../dictionary/global.dictionary';
 import { DataengineConfigurationService } from '../../../core/services';
 import { CLUSTER_CONFIGURATION } from '../../computational/computational-resource-create-dialog/cluster-configuration-templates';
+import {BucketBrowserComponent} from '../../bucket-browser/bucket-browser.component';
 
 @Component({
   selector: 'detail-dialog',
@@ -51,7 +52,8 @@ export class DetailDialogComponent implements OnInit {
     private dataengineConfigurationService: DataengineConfigurationService,
     private _fb: FormBuilder,
     public dialogRef: MatDialogRef<DetailDialogComponent>,
-    public toastr: ToastrService
+    public toastr: ToastrService,
+    private dialog: MatDialog,
   ) {
     this.notebook = data;
   }
@@ -120,4 +122,9 @@ export class DetailDialogComponent implements OnInit {
         ? (control.value && control.value !== null && CheckUtils.isJSON(control.value) ? null : { valid: false })
         : null;
   }
+
+  public bucketBrowser(type, endpoint): void {
+  this.dialog.open(BucketBrowserComponent, { data: {type: type, endpoint: endpoint}, panelClass: 'modal-fullscreen' })
+    .afterClosed().subscribe();
+  }
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.html
index 091ccb7..38e388e 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.html
@@ -46,6 +46,9 @@
     </div>
 
     <div>
+<!--      <button mat-raised-button class="butt butt-tool" (click)="bucketBrowser()">-->
+<!--        <i class="material-icons"></i>Bucket browser-->
+<!--      </button>-->
       <button mat-raised-button class="butt butt-tool" (click)="manageUngit()">
         <i class="material-icons"></i>Git credentials
       </button>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.ts
index bab05a7..6394d89 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.ts
@@ -27,6 +27,7 @@ import { Exploratory } from './resources-grid/resources-grid.model';
 import { HealthStatusService, ProjectService } from '../core/services';
 import { ManageUngitComponent } from './manage-ungit/manage-ungit.component';
 import { Project } from './../administration/project/project.component';
+import {BucketBrowserComponent} from './bucket-browser/bucket-browser.component';
 
 @Component({
   selector: 'dlab-resources',
@@ -79,6 +80,11 @@ export class ResourcesComponent implements OnInit {
       .afterClosed().subscribe(() => this.refreshGrid());
   }
 
+  public bucketBrowser(): void {
+    this.dialog.open(BucketBrowserComponent, { panelClass: 'modal-fullscreen' })
+      .afterClosed().subscribe(() => this.refreshGrid());
+  }
+
   public setActiveProject(project): void {
     this.resourcesGrid.selectActiveProject(project);
   }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts b/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts
index 7ce335b..d2d5f02 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts
@@ -27,6 +27,9 @@ import { ResourcesGridModule } from './resources-grid';
 import { ExploratoryEnvironmentCreateModule } from './exploratory/create-environment';
 import { ManageUngitComponent } from './manage-ungit/manage-ungit.component';
 import { ConfirmDeleteAccountDialog } from './manage-ungit/manage-ungit.component';
+import {BucketBrowserComponent} from './bucket-browser/bucket-browser.component';
+import {FolderTreeComponent} from './bucket-browser/folder-tree/folder-tree.component';
+import {MatTreeModule} from '@angular/material/tree';
 
 @NgModule({
   imports: [
@@ -35,14 +38,17 @@ import { ConfirmDeleteAccountDialog } from './manage-ungit/manage-ungit.componen
     ReactiveFormsModule,
     ResourcesGridModule,
     ExploratoryEnvironmentCreateModule,
-    MaterialModule
+    MaterialModule,
+    MatTreeModule
   ],
   declarations: [
     ResourcesComponent,
     ManageUngitComponent,
-    ConfirmDeleteAccountDialog
+    ConfirmDeleteAccountDialog,
+    BucketBrowserComponent,
+    FolderTreeComponent
   ],
-  entryComponents: [ManageUngitComponent, ConfirmDeleteAccountDialog],
+  entryComponents: [ManageUngitComponent, ConfirmDeleteAccountDialog, BucketBrowserComponent, FolderTreeComponent],
   exports: [ResourcesComponent]
 })
 export class ResourcesModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss b/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss
index c6f8fe8..c5ac335 100644
--- a/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss
+++ b/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss
@@ -59,6 +59,7 @@ mat-dialog-container {
     padding: 0;
     border-radius: 0;
     overflow-x: hidden;
+    position: relative;
 
     #dialog-box {
       color: $modal-text-color;


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@dlab.apache.org
For additional commands, e-mail: commits-help@dlab.apache.org