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?

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

## Move, restore, remove a folder from the main page

## Move, remove a note from the notebook view

## Restore, empty the trash

## Suffix current date if the folder exists in the trash

### 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());
+ }
}