You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by bz...@apache.org on 2016/05/02 02:45:19 UTC

incubator-zeppelin git commit: [ZEPPELIN-209] Folder support for notebooks (based on pr-190 & pr-767)

Repository: incubator-zeppelin
Updated Branches:
  refs/heads/master d481b56c5 -> f0a7caa68


[ZEPPELIN-209] Folder support for notebooks (based on pr-190 & pr-767)

### What is this PR for?

Add folder support. When create a new notebook, use '/' to separate folders.

Example: /FolderA/NotebookA

### What type of PR is it?

Feature

### Todos
* [x] - Should disable clicking the folder in dropdown menu
* [x] - Add instruction about how to add folder in the notebook create menu
* [x] - Sort the notes / dirs by name
* [x] - Move to lodash

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

### How should this be tested?

### Screenshots (if appropriate)

![e0c7ddec-e4b1-11e5-9128-2538c79d45c3](https://cloud.githubusercontent.com/assets/17979200/14037352/e75418b4-f1ff-11e5-8c42-f8ed7684aef2.gif)

### Questions:
* Does the licenses files need update?

NO

* Is there breaking changes for older versions?

NO

* Does this needs documentation?

NO

Author: johnnyws <jz...@gmail.com>
Author: Zhong Wang <wa...@gmail.com>

Closes #796 from johnnyws/seraph/folder-support and squashes the following commits:

6016652 [johnnyws] fix duplicate notes when create new note
876237b [johnnyws] put implementation fo setNotes inside of notes
49bf86b [johnnyws] code style fixes
fa63d3b [johnnyws] trigger travis
1e66dc6 [johnnyws] refactor folder construction code to make it cleaner
0e13d9f [johnnyws] add instruction in create folder dialog
961a46c [johnnyws] fix code style issues
8cdbbd2 [Zhong Wang] a couple of fixes based on the comments
2f5c9c5 [Zhong Wang] test cases for notelist factory
94a9187 [Zhong Wang] fix search box issue; fix the dropdown first elem css issue
e3e897c [Zhong Wang] refine pr-190


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

Branch: refs/heads/master
Commit: f0a7caa681230a34784c8793f6441cccb8afc688
Parents: d481b56
Author: johnnyws <jz...@gmail.com>
Authored: Wed Apr 27 14:44:24 2016 -0700
Committer: Alexander Bezzubov <bz...@apache.org>
Committed: Mon May 2 09:45:04 2016 +0900

----------------------------------------------------------------------
 zeppelin-web/src/app/home/home.controller.js    |  5 ++
 zeppelin-web/src/app/home/home.css              | 65 ++++++++++++++++++
 zeppelin-web/src/app/home/home.html             | 31 +++++++--
 zeppelin-web/src/components/navbar/navbar.html  | 19 ++++--
 .../noteName-create/note-name-dialog.html       |  1 +
 .../notebookList.datafactory.js                 | 49 ++++++++++++--
 zeppelin-web/test/spec/factory/notebookList.js  | 69 ++++++++++++++++++++
 7 files changed, 225 insertions(+), 14 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/f0a7caa6/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 14f1587..63410ce 100644
--- a/zeppelin-web/src/app/home/home.controller.js
+++ b/zeppelin-web/src/app/home/home.controller.js
@@ -71,4 +71,9 @@ angular.module('zeppelinWebApp').controller('HomeCtrl', function($scope, noteboo
     websocketMsgSrv.reloadAllNotesFromRepo();
     $scope.isReloadingNotes = true;
   };
+
+  $scope.toggleFolderNode = function(node) {
+    node.hidden = !node.hidden;
+  };
+
 });

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/f0a7caa6/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 281ffb4..c37febc 100644
--- a/zeppelin-web/src/app/home/home.css
+++ b/zeppelin-web/src/app/home/home.css
@@ -151,6 +151,70 @@ a.navbar-brand:hover {
   font: normal normal normal 14px/1 FontAwesome;
 }
 
+.dropdown-submenu {
+  position: relative;
+}
+
+.dropdown-submenu a {
+  display: block;
+  padding: 3px 20px;
+  clear: both;
+  font-weight: normal;
+  line-height: 1.42857143;
+  color: #333;
+  white-space: nowrap;
+  text-decoration: none;
+}
+
+.dropdown-submenu a:hover {
+  background-color: #f5f5f5;
+}
+
+.dropdown-submenu > .dropdown-menu {
+  top:0;
+  left:100%;
+  margin-top:-6px;
+  margin-left:-1px;
+  -webkit-border-radius:0 6px 6px 6px;
+  -moz-border-radius:0 6px 6px 6px;
+  border-radius:0 6px 6px 6px;
+}
+.dropdown-submenu:hover > .dropdown-menu {
+  display:block;
+}
+/* overwrite the style of the first element of dropdown-menu */
+.dropdown-submenu:hover > .dropdown-menu > li:first-child > a:hover {
+  color: #262626;
+  text-decoration: none;
+  background: #f5f5f5;
+}
+.dropdown-submenu > a:after {
+  display:block;
+  content:" ";
+  float:right;
+  width:0;
+  height:0;
+  border-color:transparent;
+  border-style:solid;
+  border-width:5px 0 5px 5px;
+  border-left-color:#cccccc;
+  margin-top:5px;
+  margin-right:-10px;
+}
+.dropdown-submenu:hover > a:after {
+  border-left-color:#ffffff;
+}
+.dropdown-submenu.pull-left {
+  float:none;
+}
+.dropdown-submenu.pull-left > .dropdown-menu {
+  left:-100%;
+  margin-left:10px;
+  -webkit-border-radius:6px 0 6px 6px;
+  -moz-border-radius:6px 0 6px 6px;
+  border-radius:6px 0 6px 6px;
+}
+
 @media (max-width: 767px) {
   .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {
     color: #D3D3D3;
@@ -199,6 +263,7 @@ a.navbar-brand:hover {
 #notebook-list {
   position: relative;
   overflow: hidden;
+  display: inline;
 }
 
 @media (min-width: 768px) {

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/f0a7caa6/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 71a7a1a..7255f68 100644
--- a/zeppelin-web/src/app/home/home.html
+++ b/zeppelin-web/src/app/home/home.html
@@ -12,6 +12,24 @@ See the License for the specific language governing permissions and
 limitations under the License.
 -->
 
+<script type="text/ng-template" id="notebook_folder_renderer.html">
+  <div ng-if="node.children == null">
+    <a style="text-decoration: none;" href="#/notebook/{{node.id}}">
+      <i style="font-size: 10px;" class="icon-doc"/> {{node.name || 'Note ' + node.id}}
+    </a>
+  </div>
+  <div ng-if="node.children != null">
+    <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'" /> {{node.name}}
+    </a>
+    <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>
+</script>
+
 <div ng-controller="HomeCtrl as home">
   <div ng-show="home.staticHome" class="box width-full home">
     <div class="zeppelin">
@@ -41,10 +59,15 @@ limitations under the License.
               <i style="font-size: 15px;" class="icon-notebook"></i> Create new note</a></h5>
             <ul id="notebook-names">
               <li class="filter-names" ng-include="'components/filterNoteNames/filter-note-names.html'"></li>
-              <li ng-repeat="note in home.notes.list | filter:query | orderBy:home.arrayOrderingSrv.notebookListOrdering track by $index">
-                <i style="font-size: 10px;" class="icon-doc"></i>
-                <a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}}</a>
-              </li>
+              <div ng-if="!query || query.name === ''">
+                <li ng-repeat="node in home.notes.root.children" ng-include="'notebook_folder_renderer.html'" />
+              </div>
+              <div ng-if="query && query.name !== ''">
+                <li ng-repeat="note in home.notes.flatList | filter:query | orderBy:home.arrayOrderingSrv.notebookListOrdering track by $index">
+                  <i style="font-size: 10px;" class="icon-doc"></i>
+                  <a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}}</a>
+                </li>
+              </div>
             </ul>
           </div>
         </div>

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/f0a7caa6/zeppelin-web/src/components/navbar/navbar.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/navbar/navbar.html b/zeppelin-web/src/components/navbar/navbar.html
index bdc5790..272167a 100644
--- a/zeppelin-web/src/components/navbar/navbar.html
+++ b/zeppelin-web/src/components/navbar/navbar.html
@@ -10,7 +10,19 @@ distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
--->
+  -->
+<script type="text/ng-template" id="notebook_list_renderer.html">
+  <a ng-if="note.id" href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}} </a>
+  <li ng-if="!note.id"
+      class="dropdown-submenu">
+    <a tabindex="-1" href="javascript: void(0)">{{note.name}}</a>
+    <ul class="dropdown-menu">
+      <li ng-repeat="note in note.children track by $index" ng-class="{'active' : navbar.isActive(note.id)}" ng-include="'notebook_list_renderer.html'">
+      </li>
+    </ul>
+  </li>
+</script>
+
 <div class="navbar navbar-inverse navbar-fixed-top" style="display: none;" role="navigation" ng-class="{'displayNavBar': !asIframe}">
   <div class="container">
     <div class="navbar-header">
@@ -32,10 +44,7 @@ limitations under the License.
             <li><a href="" data-toggle="modal" data-target="#noteNameModal"><i class="fa fa-plus"></i> Create new note</a></li>
             <li class="divider"></li>
             <div id="notebook-list" class="scrollbar-container">
-              <li class="filter-names" ng-include="'components/filterNoteNames/filter-note-names.html'"></li>
-              <li ng-repeat="note in navbar.notes.list | filter:query | orderBy:navbar.arrayOrderingSrv.notebookListOrdering track by $index"
-                  ng-class="{'active' : navbar.isActive(note.id)}">
-                <a href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}} </a>
+              <li ng-repeat="note in navbar.notes.root.children track by $index" ng-class="{'active' : navbar.isActive(note.id)}" ng-include="'notebook_list_renderer.html'">
               </li>
             </div>
           </ul>

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/f0a7caa6/zeppelin-web/src/components/noteName-create/note-name-dialog.html
----------------------------------------------------------------------
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 8c0cf91..f0f12a2 100644
--- a/zeppelin-web/src/components/noteName-create/note-name-dialog.html
+++ b/zeppelin-web/src/components/noteName-create/note-name-dialog.html
@@ -28,6 +28,7 @@ limitations under the License.
               placeholder="Note name" type="text" class="form-control"
               id="noteName" ng-model="note.notename" ng-enter="notenamectrl.handleNameEnter()">
           </div>
+          Use '/' to create folders. Example: /NoteDirA/Notebook1
         </div>
         <div class="modal-footer">
           <button type="button" id="createNoteButton"

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/f0a7caa6/zeppelin-web/src/components/notebookListDataFactory/notebookList.datafactory.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/notebookListDataFactory/notebookList.datafactory.js b/zeppelin-web/src/components/notebookListDataFactory/notebookList.datafactory.js
index ae48999..43fd061 100644
--- a/zeppelin-web/src/components/notebookListDataFactory/notebookList.datafactory.js
+++ b/zeppelin-web/src/components/notebookListDataFactory/notebookList.datafactory.js
@@ -14,13 +14,52 @@
 'use strict';
 
 angular.module('zeppelinWebApp').factory('notebookListDataFactory', function() {
-  var notes = {};
 
-  notes.list = [];
+  var notes = {
+    root: {children: []},
+    flatList: [],
 
-  notes.setNotes = function(notesList) {
-    notes.list = angular.copy(notesList);
+    setNotes: function(notesList) {
+      // a flat list to boost searching
+      notes.flatList = angular.copy(notesList);
+
+      // construct the folder-based tree
+      notes.root = {children: []};
+      _.reduce(notesList, function(root, note) {
+        var noteName = note.name || note.id;
+        var nodes = noteName.match(/([^\\\][^\/]|\\\/)+/g);
+
+        // recursively add nodes
+        addNode(root, nodes, note.id);
+
+        return root;
+      }, notes.root);
+    }
+  };
+
+  var addNode = function(curDir, nodes, noteId) {
+    if (nodes.length === 1) {  // the leaf
+      curDir.children.push({
+        name : nodes[0],
+        id : noteId
+      });
+    } else {  // a folder node
+      var node = nodes.shift();
+      var dir = _.find(curDir.children,
+        function(c) {return c.name === node && c.children !== undefined;});
+      if (dir !== undefined) { // found an existing dir
+        addNode(dir, nodes, noteId);
+      } else {
+        var newDir = {
+          name : node,
+          hidden : true,
+          children : []
+        };
+        curDir.children.push(newDir);
+        addNode(newDir, nodes, noteId);
+      }
+    }
   };
 
   return notes;
-});
\ No newline at end of file
+});

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/f0a7caa6/zeppelin-web/test/spec/factory/notebookList.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/test/spec/factory/notebookList.js b/zeppelin-web/test/spec/factory/notebookList.js
new file mode 100644
index 0000000..ec67866
--- /dev/null
+++ b/zeppelin-web/test/spec/factory/notebookList.js
@@ -0,0 +1,69 @@
+'use strict';
+
+describe('Factory: NotebookList', function() {
+
+  var notebookList;
+
+  beforeEach(function () {
+    module('zeppelinWebApp');
+
+    inject(function ($injector) {
+      notebookList = $injector.get('notebookListDataFactory');
+    });
+  });
+
+  it('should generate both flat list and folder-based list properly', function() {
+    var notesList = [
+      {name: 'A', id: '000001'},
+      {name: 'B', id: '000002'},
+      {id: '000003'}, // notebook without name
+      {name: '/C/CA', id: '000004'},
+      {name: '/C/CB', id: '000005'},
+      {name: '/C/CB/CBA', id: '000006'},  // same name with a dir
+      {name: '/C/CB/CBA', id: '000007'}, // same name with another note
+      {name: 'C///CB//CBB', id: '000008'}
+    ];
+    notebookList.setNotes(notesList);
+
+    var flatList = notebookList.flatList;
+    expect(flatList.length).toBe(8);
+    expect(flatList[0].name).toBe('A');
+    expect(flatList[0].id).toBe('000001');
+    expect(flatList[1].name).toBe('B');
+    expect(flatList[2].name).toBeUndefined();
+    expect(flatList[3].name).toBe('/C/CA');
+    expect(flatList[4].name).toBe('/C/CB');
+    expect(flatList[5].name).toBe('/C/CB/CBA');
+    expect(flatList[6].name).toBe('/C/CB/CBA');
+    expect(flatList[7].name).toBe('C///CB//CBB');
+
+    var folderList = notebookList.root.children;
+    expect(folderList.length).toBe(4);
+    expect(folderList[0].name).toBe('A');
+    expect(folderList[0].id).toBe('000001');
+    expect(folderList[1].name).toBe('B');
+    expect(folderList[2].name).toBe('000003');
+    expect(folderList[3].name).toBe('C');
+    expect(folderList[3].id).toBeUndefined();
+    expect(folderList[3].children.length).toBe(3);
+    expect(folderList[3].children[0].name).toBe('CA');
+    expect(folderList[3].children[0].id).toBe('000004');
+    expect(folderList[3].children[0].children).toBeUndefined();
+    expect(folderList[3].children[1].name).toBe('CB');
+    expect(folderList[3].children[1].id).toBe('000005');
+    expect(folderList[3].children[1].children).toBeUndefined();
+    expect(folderList[3].children[2].name).toBe('CB');
+    expect(folderList[3].children[2].id).toBeUndefined();
+    expect(folderList[3].children[2].children.length).toBe(3);
+    expect(folderList[3].children[2].children[0].name).toBe('CBA');
+    expect(folderList[3].children[2].children[0].id).toBe('000006');
+    expect(folderList[3].children[2].children[0].children).toBeUndefined();
+    expect(folderList[3].children[2].children[1].name).toBe('CBA');
+    expect(folderList[3].children[2].children[1].id).toBe('000007');
+    expect(folderList[3].children[2].children[1].children).toBeUndefined();
+    expect(folderList[3].children[2].children[2].name).toBe('CBB');
+    expect(folderList[3].children[2].children[2].id).toBe('000008');
+    expect(folderList[3].children[2].children[2].children).toBeUndefined();
+  });
+
+});