You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by ah...@apache.org on 2017/01/01 08:25:58 UTC

zeppelin git commit: [ZEPPELIN-1736] Introduce trash & enable removing folder

Repository: zeppelin
Updated Branches:
  refs/heads/master 082ab6342 -> 1980240fe


[ZEPPELIN-1736] Introduce trash & enable removing folder

### What is this PR for?
![image](https://cloud.githubusercontent.com/assets/8201019/20925378/03a722f8-bbfa-11e6-9afc-c62cfe6b0e2a.png)
This PR introduces the trash! (which is essential, I think ^^;)

I added basic operations of the trash:
#### Added operations of trash
* `Move` a folder to the trash
* `Move` a note to the trash
* `Restore` a folder
* `Restore` a note
* `Remove` a folder in the trash for good
* `Remove` a note in the trash for good
* `Empty` the trash
* `Restore all` in the trash
* `Move` a note to the trash from the notebook view
* `Remove` a note to the trash for good from the notebook view
* If there is a folder that has the same name, suffix current data rather than just merging(please see screenshot below!)

### What type of PR is it?
[Feature]

### Future work
Maybe it would be better if notebook view notices that a note is in the trash.

### What is the Jira issue?
[ZEPPELIN-1736](https://issues.apache.org/jira/browse/ZEPPELIN-1736)

### How should this be tested?
* Build this PR
* Move some folders or notes to the trash
* Restore them
* Empty the trash
* Restore all notes in the trash
* Remove folders or notes in the trash
* Remove a folder which has the same folder name in the trash

### Screenshots (if appropriate)
## Move, restore, remove a note from the main page
![note_from_main](https://cloud.githubusercontent.com/assets/8201019/20925760/13b0d91c-bbfc-11e6-94c5-2211c9d9a282.gif)

## Move, restore, remove a folder from the main page
![folder_test](https://cloud.githubusercontent.com/assets/8201019/20925774/2884abde-bbfc-11e6-9ba9-7b1f8a935a41.gif)

## Move, remove a note from the notebook view
![noteview_test](https://cloud.githubusercontent.com/assets/8201019/20925837/83564162-bbfc-11e6-9a92-c6bc1984b30d.gif)

## Restore, empty the trash
![restore_empty_trash](https://cloud.githubusercontent.com/assets/8201019/20925841/8f6d78d0-bbfc-11e6-90a9-6f67c95371a5.gif)

## Suffix current date if the folder exists in the trash
![same_folder_name_test](https://cloud.githubusercontent.com/assets/8201019/20926085/e8cb6954-bbfd-11e6-9cdf-acf9b785d833.gif)

### Questions:
* Does the licenses files need update? NO
* Is there breaking changes for older versions? NO
* Does this needs documentation? NO

Author: Jun Kim <i2...@gmail.com>

Closes #1730 from tae-jun/ZEPPELIN-1736 and squashes the following commits:

6bf0a70 [Jun Kim] Fix factory/noteList test error
2b5dc93 [Jun Kim] copy notesList one time. _.map() is immutable
83e2fed [Jun Kim] show trash icon after searching notes
d74bf39 [Jun Kim] Hide rename, clear output buttons
414df5f [Jun Kim] Show trash folder icon at navbar
678eb93 [Jun Kim] Change remove permanently icons, remove folder conflict infix
7afdc88 [Jun Kim] Change selenium test codes affected by chaging \u2018removeNote\u2019 button to \u2018moveToTrash\u2019
ecaed2e [Jun Kim] Fix white space bug @marchpig addressed
ab28178 [Jun Kim] Fix stylecheck issue
b1c459d [Jun Kim] Define notebook list button style
731a2da [Jun Kim] Define isTrash function on notebook controller
d5389c0 [Jun Kim] Restore a folder without removed date
f40e763 [Jun Kim] Make the order of the trash folder last
710c474 [Jun Kim] Change \u201cfor good\u201d -> \u201cpermanently\u201d
1d597ee [Jun Kim] Use trash folder id provided by constant service
77d7949 [Jun Kim] Remove if note is in the trash from the notebook view
e2b2902 [Jun Kim] Satisfy style check
3b861ee [Jun Kim] Introduce trash & removing folder


Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/1980240f
Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/1980240f
Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/1980240f

Branch: refs/heads/master
Commit: 1980240feccc128ff18a58db86467a545d74ae41
Parents: 082ab63
Author: Jun Kim <i2...@gmail.com>
Authored: Sun Jan 1 16:30:34 2017 +0900
Committer: ahyoungryu <ah...@apache.org>
Committed: Sun Jan 1 17:25:45 2017 +0900

----------------------------------------------------------------------
 .../apache/zeppelin/socket/NotebookServer.java  | 179 ++++++++++++++++++-
 .../org/apache/zeppelin/AbstractZeppelinIT.java |   4 +-
 .../apache/zeppelin/integration/ZeppelinIT.java |   4 +-
 zeppelin-web/src/app/app.js                     |   3 +-
 zeppelin-web/src/app/home/home.controller.js    |  34 +++-
 zeppelin-web/src/app/home/home.css              |   7 +
 zeppelin-web/src/app/home/home.html             |  76 ++++++--
 .../src/app/notebook/notebook-actionBar.html    |  15 +-
 .../src/app/notebook/notebook.controller.js     |  16 +-
 .../arrayOrderingSrv/arrayOrdering.service.js   |   7 +-
 .../expandCollapse/expandCollapse.directive.js  |   5 +-
 .../components/navbar/navbar-noteList-elem.html |  10 +-
 .../src/components/navbar/navbar.controller.js  |   6 +-
 .../components/noteAction/noteAction.service.js |  76 +++++++-
 .../noteListDataFactory/noteList.datafactory.js |  16 +-
 .../websocketEvents/websocketMsg.service.js     |  28 +++
 .../org/apache/zeppelin/notebook/Folder.java    |  19 +-
 .../apache/zeppelin/notebook/FolderView.java    |   2 +
 .../java/org/apache/zeppelin/notebook/Note.java |   8 +
 .../org/apache/zeppelin/notebook/Notebook.java  |  12 ++
 .../zeppelin/notebook/socket/Message.java       |   7 +
 .../apache/zeppelin/notebook/FolderTest.java    |  21 +++
 .../org/apache/zeppelin/notebook/NoteTest.java  |  24 +++
 23 files changed, 531 insertions(+), 48 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
index 1001961..3e56b05 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
@@ -29,6 +29,8 @@ import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import javax.servlet.http.HttpServletRequest;
 
@@ -74,6 +76,9 @@ import org.apache.zeppelin.utils.InterpreterBindingUtils;
 import org.apache.zeppelin.utils.SecurityUtils;
 import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
 import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
 import org.quartz.SchedulerException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -224,6 +229,27 @@ public class NotebookServer extends WebSocketServlet
           case DEL_NOTE:
             removeNote(conn, userAndRoles, notebook, messagereceived);
             break;
+          case REMOVE_FOLDER:
+            removeFolder(conn, userAndRoles, notebook, messagereceived);
+            break;
+          case MOVE_NOTE_TO_TRASH:
+            moveNoteToTrash(conn, userAndRoles, notebook, messagereceived);
+            break;
+          case MOVE_FOLDER_TO_TRASH:
+            moveFolderToTrash(conn, userAndRoles, notebook, messagereceived);
+            break;
+          case EMPTY_TRASH:
+            emptyTrash(conn, userAndRoles, notebook, messagereceived);
+            break;
+          case RESTORE_FOLDER:
+            restoreFolder(conn, userAndRoles, notebook, messagereceived);
+            break;
+          case RESTORE_NOTE:
+            restoreNote(conn, userAndRoles, notebook, messagereceived);
+            break;
+          case RESTORE_ALL:
+            restoreAll(conn, userAndRoles, notebook, messagereceived);
+            break;
           case CLONE_NOTE:
             cloneNote(conn, userAndRoles, notebook, messagereceived);
             break;
@@ -791,8 +817,15 @@ public class NotebookServer extends WebSocketServlet
     }
   }
 
-  private void renameNote(NotebookSocket conn, HashSet<String> userAndRoles, Notebook notebook,
-      Message fromMessage) throws SchedulerException, IOException {
+  private void renameNote(NotebookSocket conn, HashSet<String> userAndRoles,
+                          Notebook notebook, Message fromMessage)
+      throws SchedulerException, IOException {
+    renameNote(conn, userAndRoles, notebook, fromMessage, "rename");
+  }
+
+  private void renameNote(NotebookSocket conn, HashSet<String> userAndRoles,
+                          Notebook notebook, Message fromMessage, String op)
+      throws SchedulerException, IOException {
     String noteId = (String) fromMessage.get("id");
     String name = (String) fromMessage.get("name");
 
@@ -818,8 +851,15 @@ public class NotebookServer extends WebSocketServlet
     }
   }
 
-  private void renameFolder(NotebookSocket conn, HashSet<String> userAndRoles, Notebook notebook,
-      Message fromMessage) throws SchedulerException, IOException {
+  private void renameFolder(NotebookSocket conn, HashSet<String> userAndRoles,
+                            Notebook notebook, Message fromMessage)
+      throws SchedulerException, IOException {
+    renameFolder(conn, userAndRoles, notebook, fromMessage, "rename");
+  }
+
+  private void renameFolder(NotebookSocket conn, HashSet<String> userAndRoles,
+                          Notebook notebook, Message fromMessage, String op)
+      throws SchedulerException, IOException {
     String oldFolderId = (String) fromMessage.get("id");
     String newFolderId = (String) fromMessage.get("name");
 
@@ -831,8 +871,8 @@ public class NotebookServer extends WebSocketServlet
     for (Note note : notebook.getNotesUnderFolder(oldFolderId)) {
       String noteId = note.getId();
       if (!notebookAuthorization.isOwner(noteId, userAndRoles)) {
-        permissionError(conn, "rename folder of '" + note.getName() + "'", fromMessage.principal,
-            userAndRoles, notebookAuthorization.getOwners(noteId));
+        permissionError(conn, op + " folder of '" + note.getName() + "'", fromMessage.principal,
+                userAndRoles, notebookAuthorization.getOwners(noteId));
         return;
       }
     }
@@ -931,8 +971,131 @@ public class NotebookServer extends WebSocketServlet
     broadcastNoteList(subject, userAndRoles);
   }
 
-  private void updateParagraph(NotebookSocket conn, HashSet<String> userAndRoles, Notebook notebook,
-      Message fromMessage) throws IOException {
+  private void removeFolder(NotebookSocket conn, HashSet<String> userAndRoles,
+                            Notebook notebook, Message fromMessage)
+      throws SchedulerException, IOException {
+    String folderId = (String) fromMessage.get("id");
+    if (folderId == null) {
+      return;
+    }
+
+    NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
+    List<Note> notes = notebook.getNotesUnderFolder(folderId);
+    for (Note note : notes) {
+      String noteId = note.getId();
+      if (!notebookAuthorization.isOwner(noteId, userAndRoles)) {
+        permissionError(conn, "remove folder of '" + note.getName() + "'", fromMessage.principal,
+                userAndRoles, notebookAuthorization.getOwners(noteId));
+        return;
+      }
+    }
+
+    AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
+    for (Note note : notes) {
+      notebook.removeNote(note.getId(), subject);
+      removeNote(note.getId());
+    }
+    broadcastNoteList(subject, userAndRoles);
+  }
+
+  private void moveNoteToTrash(NotebookSocket conn, HashSet<String> userAndRoles,
+                               Notebook notebook, Message fromMessage)
+      throws SchedulerException, IOException {
+    String noteId = (String) fromMessage.get("id");
+    if (noteId == null) {
+      return;
+    }
+
+    Note note = notebook.getNote(noteId);
+    if (note != null && !note.isTrash()){
+      fromMessage.put("name", Folder.TRASH_FOLDER_ID + "/" + note.getName());
+      renameNote(conn, userAndRoles, notebook, fromMessage, "move");
+    }
+  }
+
+  private void moveFolderToTrash(NotebookSocket conn, HashSet<String> userAndRoles,
+                                 Notebook notebook, Message fromMessage)
+      throws SchedulerException, IOException {
+    String folderId = (String) fromMessage.get("id");
+    if (folderId == null) {
+      return;
+    }
+
+    Folder folder = notebook.getFolder(folderId);
+    if (folder != null && !folder.isTrash()) {
+      String trashFolderId = Folder.TRASH_FOLDER_ID + "/" + folderId;
+      if (notebook.hasFolder(trashFolderId)){
+        DateTime currentDate = new DateTime();
+        DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
+        trashFolderId += Folder.TRASH_FOLDER_CONFLICT_INFIX + formatter.print(currentDate);
+      }
+
+      fromMessage.put("name", trashFolderId);
+      renameFolder(conn, userAndRoles, notebook, fromMessage, "move");
+    }
+  }
+
+  private void restoreNote(NotebookSocket conn, HashSet<String> userAndRoles,
+                          Notebook notebook, Message fromMessage)
+      throws SchedulerException, IOException {
+    String noteId = (String) fromMessage.get("id");
+
+    if (noteId == null) {
+      return;
+    }
+
+    Note note = notebook.getNote(noteId);
+    if (note != null && note.isTrash()) {
+      fromMessage.put("name", note.getName().replaceFirst(Folder.TRASH_FOLDER_ID + "/", ""));
+      renameNote(conn, userAndRoles, notebook, fromMessage, "restore");
+    }
+  }
+
+  private void restoreFolder(NotebookSocket conn, HashSet<String> userAndRoles,
+                            Notebook notebook, Message fromMessage)
+      throws SchedulerException, IOException {
+    String folderId = (String) fromMessage.get("id");
+
+    if (folderId == null) {
+      return;
+    }
+
+    Folder folder = notebook.getFolder(folderId);
+    if (folder != null && folder.isTrash()) {
+      String restoreName = folder.getId().replaceFirst(Folder.TRASH_FOLDER_ID + "/", "").trim();
+
+      // if the folder had conflict when it had moved to trash before
+      Pattern p = Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$");
+      Matcher m = p.matcher(restoreName);
+      restoreName = m.replaceAll("").trim();
+
+      fromMessage.put("name", restoreName);
+      renameFolder(conn, userAndRoles, notebook, fromMessage, "restore");
+    }
+  }
+
+  private void restoreAll(NotebookSocket conn, HashSet<String> userAndRoles,
+                             Notebook notebook, Message fromMessage)
+      throws SchedulerException, IOException {
+    Folder trashFolder = notebook.getFolder(Folder.TRASH_FOLDER_ID);
+    if (trashFolder != null) {
+      fromMessage.data = new HashMap<>();
+      fromMessage.put("id", Folder.TRASH_FOLDER_ID);
+      fromMessage.put("name", Folder.ROOT_FOLDER_ID);
+      renameFolder(conn, userAndRoles, notebook, fromMessage, "restore trash");
+    }
+  }
+
+  private void emptyTrash(NotebookSocket conn, HashSet<String> userAndRoles,
+                          Notebook notebook, Message fromMessage)
+      throws SchedulerException, IOException {
+    fromMessage.data = new HashMap<>();
+    fromMessage.put("id", Folder.TRASH_FOLDER_ID);
+    removeFolder(conn, userAndRoles, notebook, fromMessage);
+  }
+
+  private void updateParagraph(NotebookSocket conn, HashSet<String> userAndRoles,
+                               Notebook notebook, Message fromMessage) throws IOException {
     String paragraphId = (String) fromMessage.get("id");
     if (paragraphId == null) {
       return;

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java
index 2aeaadb..645109c 100644
--- a/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java
+++ b/zeppelin-server/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java
@@ -119,10 +119,10 @@ abstract public class AbstractZeppelinIT {
   }
 
   protected void deleteTestNotebook(final WebDriver driver) {
-    driver.findElement(By.xpath(".//*[@id='main']//button[@ng-click='removeNote(note.id)']"))
+    driver.findElement(By.xpath(".//*[@id='main']//button[@ng-click='moveNoteToTrash(note.id)']"))
         .sendKeys(Keys.ENTER);
     ZeppelinITUtils.sleep(1000, true);
-    driver.findElement(By.xpath("//div[@class='modal-dialog'][contains(.,'delete this note')]" +
+    driver.findElement(By.xpath("//div[@class='modal-dialog'][contains(.,'This note will be moved to trash')]" +
         "//div[@class='modal-footer']//button[contains(.,'OK')]")).click();
     ZeppelinITUtils.sleep(100, true);
   }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java
index 07db2da..63c3ab1 100644
--- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java
+++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java
@@ -179,10 +179,10 @@ public class ZeppelinIT extends AbstractZeppelinIT {
       waitForText("BindingTest_1_",
           By.xpath(getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]"));
 
-      driver.findElement(By.xpath(".//*[@id='main']//button[@ng-click='removeNote(note.id)']"))
+      driver.findElement(By.xpath(".//*[@id='main']//button[@ng-click='moveNoteToTrash(note.id)']"))
           .sendKeys(Keys.ENTER);
       ZeppelinITUtils.sleep(1000, true);
-      driver.findElement(By.xpath("//div[@class='modal-dialog'][contains(.,'delete this note')]" +
+      driver.findElement(By.xpath("//div[@class='modal-dialog'][contains(.,'This note will be moved to trash')]" +
           "//div[@class='modal-footer']//button[contains(.,'OK')]")).click();
       ZeppelinITUtils.sleep(100, true);
 

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/app/app.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js
index ce938e2..6b5d8f9 100644
--- a/zeppelin-web/src/app/app.js
+++ b/zeppelin-web/src/app/app.js
@@ -101,7 +101,8 @@
             combineDuplications: true,
             timeout: 6000
           });
-        });
+        })
+        .constant('TRASH_FOLDER_ID', '~Trash');
 
   function auth() {
     var $http = angular.injector(['ng']).get('$http');

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/app/home/home.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/home/home.controller.js b/zeppelin-web/src/app/home/home.controller.js
index 7a9a24b..08ce309 100644
--- a/zeppelin-web/src/app/home/home.controller.js
+++ b/zeppelin-web/src/app/home/home.controller.js
@@ -22,11 +22,12 @@
     '$rootScope',
     'arrayOrderingSrv',
     'ngToast',
-    'noteActionSrv'
+    'noteActionSrv',
+    'TRASH_FOLDER_ID'
   ];
 
   function HomeCtrl($scope, noteListDataFactory, websocketMsgSrv, $rootScope, arrayOrderingSrv,
-                    ngToast, noteActionSrv) {
+                    ngToast, noteActionSrv, TRASH_FOLDER_ID) {
     ngToast.dismiss();
     var vm = this;
     vm.notes = noteListDataFactory;
@@ -41,6 +42,7 @@
     }
 
     $scope.isReloading = false;
+    $scope.TRASH_FOLDER_ID = TRASH_FOLDER_ID;
 
     var initHome = function() {
       websocketMsgSrv.getHomeNote();
@@ -91,6 +93,26 @@
       noteActionSrv.renameNote(node.id, node.path);
     };
 
+    $scope.moveNoteToTrash = function(noteId) {
+      noteActionSrv.moveNoteToTrash(noteId, false);
+    };
+
+    $scope.moveFolderToTrash = function(folderId) {
+      noteActionSrv.moveFolderToTrash(folderId);
+    };
+
+    $scope.restoreNote = function(noteId) {
+      websocketMsgSrv.restoreNote(noteId);
+    };
+
+    $scope.restoreFolder = function(folderId) {
+      websocketMsgSrv.restoreFolder(folderId);
+    };
+
+    $scope.restoreAll = function() {
+      noteActionSrv.restoreAll();
+    };
+
     $scope.renameFolder = function(node) {
       noteActionSrv.renameFolder(node.id);
     };
@@ -99,6 +121,14 @@
       noteActionSrv.removeNote(noteId, false);
     };
 
+    $scope.removeFolder = function(folderId) {
+      noteActionSrv.removeFolder(folderId);
+    };
+
+    $scope.emptyTrash = function() {
+      noteActionSrv.emptyTrash();
+    };
+
     $scope.clearAllParagraphOutput = function(noteId) {
       noteActionSrv.clearAllParagraphOutput(noteId);
     };

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/app/home/home.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/home/home.css b/zeppelin-web/src/app/home/home.css
index 493d3d8..bf88e55 100644
--- a/zeppelin-web/src/app/home/home.css
+++ b/zeppelin-web/src/app/home/home.css
@@ -51,6 +51,13 @@ body .navbar {
   width: auto;
 }
 
+.notebook-list-btn {
+  font-size: 13px;
+  margin-left: 2px;
+  cursor: pointer;
+  text-decoration: none;
+}
+
 .navbar-inverse {
   background: #3071a9;
   color: #fff;

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/app/home/home.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/home/home.html b/zeppelin-web/src/app/home/home.html
index 4507c8e..de9ecf6 100644
--- a/zeppelin-web/src/app/home/home.html
+++ b/zeppelin-web/src/app/home/home.html
@@ -13,42 +13,91 @@ limitations under the License.
 -->
 
 <script type="text/ng-template" id="notebook_folder_renderer.html">
+  <!-- note -->
   <div ng-if="node.children == null"
        ng-mouseenter="showNoteButton=true"
        ng-mouseleave="showNoteButton=false">
     <a style="text-decoration: none;" href="#/notebook/{{node.id}}">
       <i style="font-size: 10px;" class="icon-doc"/> {{noteName(node)}}
     </a>
-    <a style="text-decoration: none;">
-      <i style="font-size: 13px; margin-left: 10px; cursor: pointer; text-decoration: none;"
-         class="fa fa-pencil" ng-show="showNoteButton" ng-click="renameNote(node)"
+    <a ng-if="!node.isTrash" style="text-decoration: none;">
+      <i style="margin-left: 10px;"
+         class="fa fa-pencil notebook-list-btn" ng-show="showNoteButton" ng-click="renameNote(node)"
          tooltip-placement="bottom" tooltip="Rename note">
       </i>
     </a>
-    <a style="text-decoration: none;">
-      <i style="font-size: 13px; margin-left: 2px; cursor: pointer; text-decoration: none;"
-         class="fa fa-eraser" ng-show="showNoteButton" ng-click="clearAllParagraphOutput(node.id)"
+    <a ng-if="!node.isTrash" style="text-decoration: none;">
+      <i class="fa fa-eraser notebook-list-btn" ng-show="showNoteButton" ng-click="clearAllParagraphOutput(node.id)"
          tooltip-placement="bottom" tooltip="Clear output">
       </i>
     </a>
     <a style="text-decoration: none;">
-      <i style="font-size: 13px; margin-left: 2px; cursor: pointer; text-decoration: none;"
-         class="fa fa-trash-o" ng-show="showNoteButton" ng-click="removeNote(node.id)"
-         tooltip-placement="bottom" tooltip="Remove note">
+      <!-- if note is not in trash -->
+      <i ng-if="!node.isTrash"
+         class="fa fa-trash-o notebook-list-btn" ng-show="showNoteButton" ng-click="moveNoteToTrash(node.id)"
+         tooltip-placement="bottom" tooltip="Move note to Trash">
+      </i>
+      <!-- if note is in trash -->
+      <i ng-if="node.isTrash"
+         class="fa fa-undo notebook-list-btn" ng-show="showNoteButton" ng-click="restoreNote(node.id)"
+         tooltip-placement="bottom" tooltip="Restore note">
+      </i>
+      <i ng-if="node.isTrash" style="font-size: 16px;"
+         class="fa fa-times notebook-list-btn" ng-show="showNoteButton" ng-click="removeNote(node.id)"
+         tooltip-placement="bottom" tooltip="Remove note permanently">
       </i>
     </a>
   </div>
-  <div ng-if="node.children != null">
+  <!-- folder -->
+  <div ng-if="node.children != null && node.id !== TRASH_FOLDER_ID">
     <div ng-mouseenter="showFolderButton=true"
          ng-mouseleave="showFolderButton=false">
       <a style="text-decoration: none; cursor: pointer;" ng-click="toggleFolderNode(node)">
         <i style="font-size: 10px;" ng-class="node.hidden ? 'icon-folder' : 'icon-folder-alt'" /> {{noteName(node)}}
       </a>
       <a style="text-decoration: none;">
-        <i style="font-size: 13px; margin-left: 10px; cursor: pointer; text-decoration: none;"
-           class="fa fa-pencil" ng-show="showFolderButton" ng-click="renameFolder(node)"
+        <i ng-if="!node.isTrash" style="margin-left: 10px;"
+           class="fa fa-pencil notebook-list-btn" ng-show="showFolderButton" ng-click="renameFolder(node)"
            tooltip-placement="bottom" tooltip="Rename folder">
         </i>
+        <!-- if folder is not in trash -->
+        <i ng-if="!node.isTrash"
+           class="fa fa-trash-o notebook-list-btn" ng-show="showFolderButton" ng-click="moveFolderToTrash(node.id)"
+           tooltip-placement="bottom" tooltip="Move folder to Trash">
+        </i>
+        <!-- if folder is in trash -->
+        <i ng-if="node.isTrash"
+           class="fa fa-undo notebook-list-btn" ng-show="showFolderButton" ng-click="restoreFolder(node.id)"
+           tooltip-placement="bottom" tooltip="Restore folder">
+        </i>
+        <i ng-if="node.isTrash" style="font-size: 16px"
+           class="fa fa-times notebook-list-btn" ng-show="showFolderButton" ng-click="removeFolder(node.id)"
+           tooltip-placement="bottom" tooltip="Remove folder permanently">
+        </i>
+      </a>
+    </div>
+    <div ng-if="!node.hidden">
+      <ul style="list-style-type: none; padding-left:15px;">
+        <li ng-repeat="node in node.children" ng-include="'notebook_folder_renderer.html'" />
+      </ul>
+    </div>
+  </div>
+  <!-- trash folder -->
+  <div ng-if="node.id === TRASH_FOLDER_ID">
+    <div ng-mouseenter="showFolderButton=true"
+         ng-mouseleave="showFolderButton=false">
+      <a style="text-decoration: none; cursor: pointer;" ng-click="toggleFolderNode(node)">
+        <i style="font-size: 14px;" class="fa fa-trash-o" /> Trash
+      </a>
+      <a style="text-decoration: none;">
+        <i style="margin-left: 10px"
+           class="fa fa-undo notebook-list-btn" ng-show="showFolderButton" ng-click="restoreAll()"
+           tooltip-placement="bottom" tooltip="Restore all">
+        </i>
+        <i style="font-size: 16px;"
+           class="fa fa-times notebook-list-btn" ng-show="showFolderButton" ng-click="emptyTrash()"
+           tooltip-placement="bottom" tooltip="Empty trash">
+        </i>
       </a>
     </div>
     <div ng-if="!node.hidden">
@@ -97,7 +146,8 @@ limitations under the License.
               </div>
               <div ng-if="query && query.name !== ''">
                 <li ng-repeat="note in home.notes.flatList | filter:query.q | orderBy:home.arrayOrderingSrv.noteListOrdering track by $index">
-                  <i style="font-size: 10px;" class="icon-doc"></i>
+                  <i ng-if="!note.isTrash" style="font-size: 10px;" class="icon-doc"></i>
+                  <i ng-if="note.isTrash" style="font-size: 12px;" class="fa fa-trash-o"></i>
                   <a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{noteName(note)}}</a>
                 </li>
               </div>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/app/notebook/notebook-actionBar.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html
index 88e47b9..821dae1 100644
--- a/zeppelin-web/src/app/notebook/notebook-actionBar.html
+++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html
@@ -153,11 +153,22 @@ limitations under the License.
 
 <!-- put the delete action by itself for your protection -->
       <span class="labelBtn" style="vertical-align:middle; display:inline-block;">
-        <button type="button"
+        <!-- if the note is in the trash, remove note permanently -->
+        <button ng-if="isTrash(note)"
+                type="button"
                 class="btn btn-default btn-xs"
                 ng-click="removeNote(note.id)"
                 ng-hide="viewOnly"
-                tooltip-placement="bottom" tooltip="Remove this note">
+                tooltip-placement="bottom" tooltip="Remove this note permanently">
+          <i class="icon-trash"></i>
+        </button>
+        <!-- if the note is not in the trash, move to trash -->
+        <button ng-if="!isTrash(note)"
+                type="button"
+                class="btn btn-default btn-xs"
+                ng-click="moveNoteToTrash(note.id)"
+                ng-hide="viewOnly"
+                tooltip-placement="bottom" tooltip="Move this note to trash">
           <i class="icon-trash"></i>
         </button>
       </span>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/app/notebook/notebook.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js
index f951035..af91293 100644
--- a/zeppelin-web/src/app/notebook/notebook.controller.js
+++ b/zeppelin-web/src/app/notebook/notebook.controller.js
@@ -28,12 +28,13 @@
     'saveAsService',
     'ngToast',
     'noteActionSrv',
-    'noteVarShareService'
+    'noteVarShareService',
+    'TRASH_FOLDER_ID'
   ];
 
   function NotebookCtrl($scope, $route, $routeParams, $location, $rootScope,
                         $http, websocketMsgSrv, baseUrlSrv, $timeout, saveAsService,
-                        ngToast, noteActionSrv, noteVarShareService) {
+                        ngToast, noteActionSrv, noteVarShareService, TRASH_FOLDER_ID) {
 
     ngToast.dismiss();
 
@@ -182,11 +183,20 @@
       $scope.$broadcast('doubleClickParagraph', paragraphId);
     };
 
-    // Remove the note and go back to the main page
+    // Move the note to trash and go back to the main page
+    $scope.moveNoteToTrash = function(noteId) {
+      noteActionSrv.moveNoteToTrash(noteId, true);
+    };
+
+    // Remove the note permanently if it's in the trash
     $scope.removeNote = function(noteId) {
       noteActionSrv.removeNote(noteId, true);
     };
 
+    $scope.isTrash = function(note) {
+      return note ? note.name.split('/')[0] === TRASH_FOLDER_ID : false;
+    };
+
     //Export notebook
     $scope.exportNote = function() {
       var jsonContent = JSON.stringify($scope.note);

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/components/arrayOrderingSrv/arrayOrdering.service.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/arrayOrderingSrv/arrayOrdering.service.js b/zeppelin-web/src/components/arrayOrderingSrv/arrayOrdering.service.js
index 6c990e3..6f89ebb 100644
--- a/zeppelin-web/src/components/arrayOrderingSrv/arrayOrdering.service.js
+++ b/zeppelin-web/src/components/arrayOrderingSrv/arrayOrdering.service.js
@@ -15,10 +15,15 @@
 
   angular.module('zeppelinWebApp').service('arrayOrderingSrv', arrayOrderingSrv);
 
-  function arrayOrderingSrv() {
+  arrayOrderingSrv.$inject = ['TRASH_FOLDER_ID'];
+
+  function arrayOrderingSrv(TRASH_FOLDER_ID) {
     var arrayOrderingSrv = this;
 
     this.noteListOrdering = function(note) {
+      if (note.id === TRASH_FOLDER_ID) {
+        return '\uFFFF';
+      }
       return arrayOrderingSrv.getNoteName(note);
     };
 

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/components/expandCollapse/expandCollapse.directive.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/expandCollapse/expandCollapse.directive.js b/zeppelin-web/src/components/expandCollapse/expandCollapse.directive.js
index c34742d..1230934 100644
--- a/zeppelin-web/src/components/expandCollapse/expandCollapse.directive.js
+++ b/zeppelin-web/src/components/expandCollapse/expandCollapse.directive.js
@@ -25,7 +25,10 @@
             angular.element(element).find('i.icon-folder-alt').toggleClass('icon-folder icon-folder-alt');
           } else {
             angular.element(element).find('.expandable').first().slideToggle('200',function() {
-              angular.element(element).find('i').first().toggleClass('icon-folder icon-folder-alt');
+              // do not toggle trash folder
+              if (angular.element(element).find('.fa-trash-o').length === 0) {
+                angular.element(element).find('i').first().toggleClass('icon-folder icon-folder-alt');
+              }
             });
           }
           event.stopPropagation();

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/components/navbar/navbar-noteList-elem.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/navbar/navbar-noteList-elem.html b/zeppelin-web/src/components/navbar/navbar-noteList-elem.html
index ece94a9..6898217 100644
--- a/zeppelin-web/src/components/navbar/navbar-noteList-elem.html
+++ b/zeppelin-web/src/components/navbar/navbar-noteList-elem.html
@@ -20,8 +20,14 @@ limitations under the License.
   <expand-collapse>
       <div>
         <a class="notebook-list-item" href="javascript:void(0)">
-          <i style="font-size: 10px; margin-right: 5px;" class="icon-folder"></i>
-          <span>{{noteName(note)}}</span>
+          <div ng-if="note.id !== navbar.TRASH_FOLDER_ID">
+            <i style="font-size: 10px; margin-right: 5px;" class="icon-folder"></i>
+            <span>{{noteName(note)}}</span>
+          </div>
+          <div ng-if="note.id === navbar.TRASH_FOLDER_ID">
+            <i style="font-size: 12px; margin-right: 5px;" class="fa fa-trash-o"></i>
+            <span>Trash</span>
+          </div>
         </a>
       </div>
       <div class="expandable" style="color: black;">

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/components/navbar/navbar.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/navbar/navbar.controller.js b/zeppelin-web/src/components/navbar/navbar.controller.js
index f583023..14daf44 100644
--- a/zeppelin-web/src/components/navbar/navbar.controller.js
+++ b/zeppelin-web/src/components/navbar/navbar.controller.js
@@ -25,12 +25,13 @@
     'baseUrlSrv',
     'websocketMsgSrv',
     'arrayOrderingSrv',
-    'searchService'
+    'searchService',
+    'TRASH_FOLDER_ID'
   ];
 
   function NavCtrl($scope, $rootScope, $http, $routeParams, $location,
                    noteListDataFactory, baseUrlSrv, websocketMsgSrv,
-                   arrayOrderingSrv, searchService) {
+                   arrayOrderingSrv, searchService, TRASH_FOLDER_ID) {
     var vm = this;
     vm.arrayOrderingSrv = arrayOrderingSrv;
     vm.connected = websocketMsgSrv.isConnected();
@@ -40,6 +41,7 @@
     vm.search = search;
     vm.searchForm = searchService;
     vm.showLoginWindow = showLoginWindow;
+    vm.TRASH_FOLDER_ID = TRASH_FOLDER_ID;
 
     $scope.query = {q: ''};
 

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/components/noteAction/noteAction.service.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/noteAction/noteAction.service.js b/zeppelin-web/src/components/noteAction/noteAction.service.js
index 9fe2dec..97bd5f6 100644
--- a/zeppelin-web/src/components/noteAction/noteAction.service.js
+++ b/zeppelin-web/src/components/noteAction/noteAction.service.js
@@ -18,11 +18,41 @@
   noteActionSrv.$inject = ['websocketMsgSrv', '$location', 'renameSrv', 'noteListDataFactory'];
 
   function noteActionSrv(websocketMsgSrv, $location, renameSrv, noteListDataFactory) {
+    this.moveNoteToTrash = function(noteId, redirectToHome) {
+      BootstrapDialog.confirm({
+        closable: true,
+        title: 'Move this note to trash?',
+        message: 'This note will be moved to <strong>trash</strong>.',
+        callback: function(result) {
+          if (result) {
+            websocketMsgSrv.moveNoteToTrash(noteId);
+            if (redirectToHome) {
+              $location.path('/');
+            }
+          }
+        }
+      });
+    };
+
+    this.moveFolderToTrash = function(folderId) {
+      BootstrapDialog.confirm({
+        closable: true,
+        title: 'Move this folder to trash?',
+        message: 'This folder will be moved to <strong>trash</strong>.',
+        callback: function(result) {
+          if (result) {
+            websocketMsgSrv.moveFolderToTrash(folderId);
+          }
+        }
+      });
+    };
+
     this.removeNote = function(noteId, redirectToHome) {
       BootstrapDialog.confirm({
+        type: BootstrapDialog.TYPE_WARNING,
         closable: true,
-        title: '',
-        message: 'Do you want to delete this note?',
+        title: 'WARNING! This note will be removed permanently',
+        message: 'This cannot be undone. Are you sure?',
         callback: function(result) {
           if (result) {
             websocketMsgSrv.deleteNote(noteId);
@@ -34,6 +64,48 @@
       });
     };
 
+    this.removeFolder = function(folderId) {
+      BootstrapDialog.confirm({
+        type: BootstrapDialog.TYPE_WARNING,
+        closable: true,
+        title: 'WARNING! This folder will be removed permanently',
+        message: 'This cannot be undone. Are you sure?',
+        callback: function(result) {
+          if (result) {
+            websocketMsgSrv.removeFolder(folderId);
+          }
+        }
+      });
+    };
+
+    this.restoreAll = function() {
+      BootstrapDialog.confirm({
+        closable: true,
+        title: 'Are you sure want to restore all notes in the trash?',
+        message: 'Folders and notes in the trash will be ' +
+          '<strong>merged</strong> into their original position.',
+        callback: function(result) {
+          if (result) {
+            websocketMsgSrv.restoreAll();
+          }
+        }
+      });
+    };
+
+    this.emptyTrash = function() {
+      BootstrapDialog.confirm({
+        type: BootstrapDialog.TYPE_WARNING,
+        closable: true,
+        title: 'WARNING! Notes under trash will be removed permanently',
+        message: 'This cannot be undone. Are you sure?',
+        callback: function(result) {
+          if (result) {
+            websocketMsgSrv.emptyTrash();
+          }
+        }
+      });
+    };
+
     this.clearAllParagraphOutput = function(noteId) {
       BootstrapDialog.confirm({
         closable: true,

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/components/noteListDataFactory/noteList.datafactory.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/noteListDataFactory/noteList.datafactory.js b/zeppelin-web/src/components/noteListDataFactory/noteList.datafactory.js
index 67a7d5f..9d74600 100644
--- a/zeppelin-web/src/components/noteListDataFactory/noteList.datafactory.js
+++ b/zeppelin-web/src/components/noteListDataFactory/noteList.datafactory.js
@@ -15,7 +15,9 @@
 
   angular.module('zeppelinWebApp').factory('noteListDataFactory', noteListDataFactory);
 
-  function noteListDataFactory() {
+  noteListDataFactory.$inject = ['TRASH_FOLDER_ID'];
+
+  function noteListDataFactory(TRASH_FOLDER_ID) {
     var notes = {
       root: {children: []},
       flatList: [],
@@ -23,7 +25,11 @@
 
       setNotes: function(notesList) {
         // a flat list to boost searching
-        notes.flatList = angular.copy(notesList);
+        notes.flatList = _.map(notesList, (note) => {
+          note.isTrash = note.name ?
+            note.name.split('/')[0] === TRASH_FOLDER_ID : false;
+          return note;
+        });
 
         // construct the folder-based tree
         notes.root = {children: []};
@@ -45,7 +51,8 @@
         curDir.children.push({
           name: nodes[0],
           id: noteId,
-          path: curDir.id ? curDir.id + '/' + nodes[0] : nodes[0]
+          path: curDir.id ? curDir.id + '/' + nodes[0] : nodes[0],
+          isTrash: curDir.id ? curDir.id.split('/')[0] === TRASH_FOLDER_ID : false
         });
       } else {  // a folder node
         var node = nodes.shift();
@@ -58,7 +65,8 @@
             id: curDir.id ? curDir.id + '/' + node : node,
             name: node,
             hidden: true,
-            children: []
+            children: [],
+            isTrash: curDir.id ? curDir.id.split('/')[0] === TRASH_FOLDER_ID : false
           };
 
           // add the folder to flat folder map

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js
index 6ac4d55..24bbc17 100644
--- a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js
+++ b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js
@@ -34,10 +34,38 @@
         });
       },
 
+      moveNoteToTrash: function(noteId) {
+        websocketEvents.sendNewEvent({op: 'MOVE_NOTE_TO_TRASH', data: {id: noteId}});
+      },
+
+      moveFolderToTrash: function(folderId) {
+        websocketEvents.sendNewEvent({op: 'MOVE_FOLDER_TO_TRASH', data: {id: folderId}});
+      },
+
+      restoreNote: function(noteId) {
+        websocketEvents.sendNewEvent({op: 'RESTORE_NOTE', data: {id: noteId}});
+      },
+
+      restoreFolder: function(folderId) {
+        websocketEvents.sendNewEvent({op: 'RESTORE_FOLDER', data: {id: folderId}});
+      },
+
+      restoreAll: function() {
+        websocketEvents.sendNewEvent({op: 'RESTORE_ALL'});
+      },
+
       deleteNote: function(noteId) {
         websocketEvents.sendNewEvent({op: 'DEL_NOTE', data: {id: noteId}});
       },
 
+      removeFolder: function(folderId) {
+        websocketEvents.sendNewEvent({op: 'REMOVE_FOLDER', data: {id: folderId}});
+      },
+
+      emptyTrash: function() {
+        websocketEvents.sendNewEvent({op: 'EMPTY_TRASH'});
+      },
+
       cloneNote: function(noteIdToClone, newNoteName) {
         websocketEvents.sendNewEvent({op: 'CLONE_NOTE', data: {id: noteIdToClone, name: newNoteName}});
       },

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Folder.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Folder.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Folder.java
index 15b2610..1853550 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Folder.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Folder.java
@@ -30,6 +30,8 @@ import java.util.*;
  */
 public class Folder {
   public static final String ROOT_FOLDER_ID = "/";
+  public static final String TRASH_FOLDER_ID = "~Trash";
+  public static final String TRASH_FOLDER_CONFLICT_INFIX = " ";
 
   private String id;
 
@@ -52,7 +54,7 @@ public class Folder {
   }
 
   public String getName() {
-    if (getId().equals(ROOT_FOLDER_ID))
+    if (isRoot())
       return ROOT_FOLDER_ID;
 
     String path = getId();
@@ -66,7 +68,7 @@ public class Folder {
   }
 
   public String getParentFolderId() {
-    if (getId().equals(ROOT_FOLDER_ID))
+    if (isRoot())
       return ROOT_FOLDER_ID;
 
     int lastSlashIndex = getId().lastIndexOf("/");
@@ -106,7 +108,7 @@ public class Folder {
    * @param newId
    */
   public void rename(String newId) {
-    if (getId().equals(ROOT_FOLDER_ID))   // root folder cannot be renamed
+    if (isRoot())   // root folder cannot be renamed
       return;
 
     String oldId = getId();
@@ -221,4 +223,15 @@ public class Folder {
   public boolean hasChild() {
     return children.size() > 0;
   }
+
+  boolean isRoot() {
+    return getId().equals(ROOT_FOLDER_ID);
+  }
+
+  public boolean isTrash() {
+    if (isRoot())
+      return false;
+
+    return getId().split("/")[0].equals(TRASH_FOLDER_ID);
+  }
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FolderView.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FolderView.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FolderView.java
index 7fc5fe6..7d3f001 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FolderView.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FolderView.java
@@ -99,6 +99,8 @@ public class FolderView implements NoteNameListener, FolderListener {
   }
 
   private Folder createFolder(String folderId) {
+    folderId = Folder.normalizeFolderId(folderId);
+
     Folder newFolder = new Folder(folderId);
     newFolder.addFolderListener(this);
 

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java
index 855c0b4..22934d3 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java
@@ -596,6 +596,14 @@ public class Note implements Serializable, ParagraphJobListener {
     return true;
   }
 
+  public boolean isTrash() {
+    String path = getName();
+    if (path.charAt(0) == '/') {
+      path = path.substring(1);
+    }
+    return path.split("/")[0].equals(Folder.TRASH_FOLDER_ID);
+  }
+
   public List<InterpreterCompletion> completion(String paragraphId, String buffer, int cursor) {
     Paragraph p = getParagraph(paragraphId);
     p.setListener(jobListenerFactory.getParagraphJobListener(this));

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java
index 25255f9..8b946f2 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java
@@ -307,6 +307,18 @@ public class Notebook implements NoteEventListener {
     }
   }
 
+  public Folder getFolder(String folderId) {
+    synchronized (folders) {
+      return folders.getFolder(folderId);
+    }
+  }
+
+  public boolean hasFolder(String folderId) {
+    synchronized (folders) {
+      return folders.hasFolder(folderId);
+    }
+  }
+
   public void removeNote(String id, AuthenticationInfo subject) {
     Preconditions.checkNotNull(subject, "AuthenticationInfo should not be null");
 

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
index 6f36643..d123020 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
@@ -46,6 +46,13 @@ public class Message {
     NEW_NOTE,         // [c-s] create new notebook
     DEL_NOTE,         // [c-s] delete notebook
                       // @param id note id
+    REMOVE_FOLDER,
+    MOVE_NOTE_TO_TRASH,
+    MOVE_FOLDER_TO_TRASH,
+    RESTORE_FOLDER,
+    RESTORE_NOTE,
+    RESTORE_ALL,
+    EMPTY_TRASH,
     CLONE_NOTE,       // [c-s] clone new notebook
                       // @param id id of note to clone
                       // @param name name for the cloned note

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/FolderTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/FolderTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/FolderTest.java
index 6b116a8..8325e8a 100644
--- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/FolderTest.java
+++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/FolderTest.java
@@ -30,6 +30,8 @@ import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 @RunWith(MockitoJUnitRunner.class)
 public class FolderTest {
@@ -186,4 +188,23 @@ public class FolderTest {
     assertEquals("a", aFolder.getName());
     assertEquals("b", abFolder.getName());
   }
+
+  @Test
+  public void isTrashTest() {
+    Folder folder;
+    // Not trash
+    folder = new Folder(Folder.ROOT_FOLDER_ID);
+    assertFalse(folder.isTrash());
+    folder = new Folder("a");
+    assertFalse(folder.isTrash());
+    folder = new Folder("a/b");
+    assertFalse(folder.isTrash());
+    // trash
+    folder = new Folder(Folder.TRASH_FOLDER_ID);
+    assertTrue(folder.isTrash());
+    folder = new Folder(Folder.TRASH_FOLDER_ID + "/a");
+    assertTrue(folder.isTrash());
+    folder = new Folder(Folder.TRASH_FOLDER_ID + "/a/b");
+    assertTrue(folder.isTrash());
+  }
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/1980240f/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteTest.java
index 354baae..ad6031e 100644
--- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteTest.java
+++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NoteTest.java
@@ -175,4 +175,28 @@ public class NoteTest {
     note.setName("a/b/note");
     assertEquals("note", note.getNameWithoutPath());
   }
+
+  @Test
+  public void isTrashTest() {
+    Note note = new Note(repo, interpreterFactory, jobListenerFactory, index, credentials, noteEventListener);
+    // Notes in the root folder
+    note.setName("noteOnRootFolder");
+    assertFalse(note.isTrash());
+    note.setName("/noteOnRootFolderStartsWithSlash");
+    assertFalse(note.isTrash());
+
+    // Notes in subdirectories
+    note.setName("/a/b/note");
+    assertFalse(note.isTrash());
+    note.setName("a/b/note");
+    assertFalse(note.isTrash());
+
+    // Notes in trash
+    note.setName(Folder.TRASH_FOLDER_ID + "/a");
+    assertTrue(note.isTrash());
+    note.setName("/" + Folder.TRASH_FOLDER_ID + "/a");
+    assertTrue(note.isTrash());
+    note.setName(Folder.TRASH_FOLDER_ID + "/a/b/c");
+    assertTrue(note.isTrash());
+  }
 }