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 ebc57559c6f..1020e27178b 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 @@ -49,6 +49,7 @@ import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; import org.apache.zeppelin.notebook.JobListenerFactory; +import org.apache.zeppelin.notebook.Folder; import org.apache.zeppelin.notebook.Note; import org.apache.zeppelin.notebook.Notebook; import org.apache.zeppelin.notebook.NotebookAuthorization; @@ -248,6 +249,9 @@ public void onMessage(NotebookSocket conn, String msg) { case NOTE_RENAME: renameNote(conn, userAndRoles, notebook, messagereceived); break; + case FOLDER_RENAME: + renameFolder(conn, userAndRoles, notebook, messagereceived); + break; case COMPLETION: completion(conn, userAndRoles, notebook, messagereceived); break; @@ -635,7 +639,7 @@ void permissionError(NotebookSocket conn, String op, op, userAndRoles, allowed); conn.send(serializeMessage(new Message(OP.AUTH_INFO).put("info", - "Insufficient privileges to " + op + " notebook.\n\n" + + "Insufficient privileges to " + op + "note.\n\n" + "Allowed users or roles: " + allowed.toString() + "\n\n" + "But the user " + userName + " belongs to: " + userAndRoles.toString()))); } @@ -749,7 +753,7 @@ private void renameNote(NotebookSocket conn, HashSet userAndRoles, NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization(); if (!notebookAuthorization.isOwner(noteId, userAndRoles)) { - permissionError(conn, "renameNote", fromMessage.principal, + permissionError(conn, "rename", fromMessage.principal, userAndRoles, notebookAuthorization.getOwners(noteId)); return; } @@ -765,6 +769,41 @@ private void renameNote(NotebookSocket conn, HashSet userAndRoles, } } + private void renameFolder(NotebookSocket conn, HashSet userAndRoles, + Notebook notebook, Message fromMessage) + throws SchedulerException, IOException { + String oldFolderId = (String) fromMessage.get("id"); + String newFolderId = (String) fromMessage.get("name"); + + if (oldFolderId == null) { + return; + } + + NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization(); + 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)); + return; + } + } + + Folder oldFolder = notebook.renameFolder(oldFolderId, newFolderId); + + if (oldFolder != null) { + AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal); + + List renamedNotes = oldFolder.getNotesRecursively(); + for (Note note : renamedNotes) { + note.persist(subject); + broadcastNote(note); + } + + broadcastNoteList(subject, userAndRoles); + } + } + private boolean isCronUpdated(Map configA, Map configB) { boolean cronUpdated = false; diff --git a/zeppelin-web/src/app/home/home.controller.js b/zeppelin-web/src/app/home/home.controller.js index 839978fcedd..170a087fba5 100644 --- a/zeppelin-web/src/app/home/home.controller.js +++ b/zeppelin-web/src/app/home/home.controller.js @@ -92,6 +92,10 @@ noteActionSrv.renameNote(node.id, node.path); }; + $scope.renameFolder = function(node) { + noteActionSrv.renameFolder(node.id); + }; + $scope.removeNote = function(noteId) { noteActionSrv.removeNote(noteId, false); }; diff --git a/zeppelin-web/src/app/home/home.html b/zeppelin-web/src/app/home/home.html index 0b9883e9b8c..4507c8e6863 100644 --- a/zeppelin-web/src/app/home/home.html +++ b/zeppelin-web/src/app/home/home.html @@ -39,9 +39,18 @@
- - {{noteName(node)}} - +
  • diff --git a/zeppelin-web/src/components/noteAction/noteAction.service.js b/zeppelin-web/src/components/noteAction/noteAction.service.js index 3630fc8e05e..ca0278256ec 100644 --- a/zeppelin-web/src/components/noteAction/noteAction.service.js +++ b/zeppelin-web/src/components/noteAction/noteAction.service.js @@ -16,9 +16,9 @@ angular.module('zeppelinWebApp').service('noteActionSrv', noteActionSrv); - noteActionSrv.$inject = ['websocketMsgSrv', '$location', 'renameSrv']; + noteActionSrv.$inject = ['websocketMsgSrv', '$location', 'renameSrv', 'noteListDataFactory']; - function noteActionSrv(websocketMsgSrv, $location, renameSrv) { + function noteActionSrv(websocketMsgSrv, $location, renameSrv, noteListDataFactory) { this.removeNote = function(noteId, redirectToHome) { BootstrapDialog.confirm({ closable: true, @@ -57,5 +57,58 @@ } }); }; + + this.renameFolder = function(folderId) { + renameSrv.openRenameModal({ + title: 'Rename folder', + oldName: folderId, + callback: function(newName) { + var newFolderId = normalizeFolderId(newName); + if (_.has(noteListDataFactory.flatFolderMap, newFolderId)) { + BootstrapDialog.confirm({ + type: BootstrapDialog.TYPE_WARNING, + closable: true, + title: 'WARNING! The folder will be MERGED', + message: 'The folder will be merged into ' + newFolderId + '. Are you sure?', + callback: function(result) { + if (result) { + websocketMsgSrv.renameFolder(folderId, newFolderId); + } + } + }); + } else { + websocketMsgSrv.renameFolder(folderId, newFolderId); + } + } + }); + }; + + function normalizeFolderId(folderId) { + folderId = folderId.trim(); + + while (folderId.contains('\\')) { + folderId = folderId.replace('\\', '/'); + } + + while (folderId.contains('///')) { + folderId = folderId.replace('///', '/'); + } + + folderId = folderId.replace('//', '/'); + + if (folderId === '/') { + return '/'; + } + + if (folderId[0] === '/') { + folderId = folderId.substring(1); + } + + if (folderId.slice(-1) === '/') { + folderId = folderId.slice(0, -1); + } + + return folderId; + } } })(); diff --git a/zeppelin-web/src/components/noteListDataFactory/noteList.datafactory.js b/zeppelin-web/src/components/noteListDataFactory/noteList.datafactory.js index 0f700b6cf7f..2ffb53978af 100644 --- a/zeppelin-web/src/components/noteListDataFactory/noteList.datafactory.js +++ b/zeppelin-web/src/components/noteListDataFactory/noteList.datafactory.js @@ -20,6 +20,7 @@ var notes = { root: {children: []}, flatList: [], + flatFolderMap: {}, setNotes: function(notesList) { // a flat list to boost searching @@ -27,6 +28,7 @@ // construct the folder-based tree notes.root = {children: []}; + notes.flatFolderMap = {}; _.reduce(notesList, function(root, note) { var noteName = note.name || note.id; var nodes = noteName.match(/([^\/][^\/]*)/g); @@ -59,6 +61,10 @@ hidden: true, children: [] }; + + // add the folder to flat folder map + notes.flatFolderMap[newDir.id] = newDir; + curDir.children.push(newDir); addNode(newDir, nodes, noteId); } diff --git a/zeppelin-web/src/components/noteName-create/note-name-dialog.html b/zeppelin-web/src/components/noteName-create/note-name-dialog.html index fdb825a4f81..0bb599cfa26 100644 --- a/zeppelin-web/src/components/noteName-create/note-name-dialog.html +++ b/zeppelin-web/src/components/noteName-create/note-name-dialog.html @@ -38,7 +38,7 @@
- Use '/' to create folders. Example: /NoteDirA/Notebook1 + Use '/' to create folders. Example: /NoteDirA/Note1