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 1859ba01961..0f8ce70fc39 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 @@ -45,6 +45,7 @@ import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener; import org.apache.zeppelin.notebook.*; +import org.apache.zeppelin.notebook.repo.NotebookRepo; import org.apache.zeppelin.notebook.repo.NotebookRepo.Revision; import org.apache.zeppelin.notebook.socket.Message; import org.apache.zeppelin.notebook.socket.Message.OP; @@ -221,6 +222,9 @@ public void onMessage(NotebookSocket conn, String msg) { case CHECKPOINT_NOTEBOOK: checkpointNotebook(conn, notebook, messagereceived); break; + case LIST_REVISION_HISTORY: + listRevisionHistory(conn, notebook, messagereceived); + break; case NOTE_REVISION: getNoteRevision(conn, notebook, messagereceived); break; @@ -1130,7 +1134,22 @@ private void checkpointNotebook(NotebookSocket conn, Notebook notebook, String noteId = (String) fromMessage.get("noteId"); String commitMessage = (String) fromMessage.get("commitMessage"); AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal); - notebook.checkpointNote(noteId, commitMessage, subject); + Revision revision = notebook.checkpointNote(noteId, commitMessage, subject); + if (revision != null) { + List revisions = notebook.listRevisionHistory(noteId, subject); + conn.send(serializeMessage(new Message(OP.LIST_REVISION_HISTORY) + .put("revisionList", revisions))); + } + } + + private void listRevisionHistory(NotebookSocket conn, Notebook notebook, + Message fromMessage) throws IOException { + String noteId = (String) fromMessage.get("noteId"); + AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal); + List revisions = notebook.listRevisionHistory(noteId, subject); + + conn.send(serializeMessage(new Message(OP.LIST_REVISION_HISTORY) + .put("revisionList", revisions))); } private void getNoteRevision(NotebookSocket conn, Notebook notebook, Message fromMessage) diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index 170d56d66ef..cf13d50aefa 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -64,34 +64,63 @@

tooltip-placement="bottom" tooltip="Export the notebook"> - - + + + + diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 472765097b0..53399b2e21d 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -19,6 +19,7 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro $rootScope, $http, websocketMsgSrv, baseUrlSrv, $timeout, SaveAsService) { $scope.note = null; + $scope.moment = moment; $scope.showEditor = false; $scope.editorToggled = false; $scope.tableToggled = false; @@ -54,6 +55,14 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro var previousSelectedListWriters = []; var searchText = []; $scope.role = ''; + $scope.noteRevisions = []; + + $scope.$on('setConnectedStatus', function(event, param) { + if (connectedOnce && param) { + initNotebook(); + } + connectedOnce = true; + }); $scope.getCronOptionNameFromValue = function(value) { if (!value) { @@ -71,6 +80,7 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro /** Init the new controller */ var initNotebook = function() { websocketMsgSrv.getNotebook($routeParams.noteId); + websocketMsgSrv.listRevisionHistory($routeParams.noteId); var currentRoute = $route.current; if (currentRoute) { @@ -175,6 +185,11 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro document.getElementById('note.checkpoint.message').value = ''; }; + $scope.$on('listRevisionHistory', function(event, data) { + console.log('We got the revisions %o', data); + $scope.noteRevisions = data.revisionList; + }); + // receive certain revision of note $scope.$on('noteRevision', function(event, data) { console.log('received note revision %o', data); diff --git a/zeppelin-web/src/app/notebook/notebook.css b/zeppelin-web/src/app/notebook/notebook.css index 59f756168f5..ed45c67bc70 100644 --- a/zeppelin-web/src/app/notebook/notebook.css +++ b/zeppelin-web/src/app/notebook/notebook.css @@ -16,6 +16,48 @@ padding-top: 36px; } +.revision { + max-width: 250px; + margin-bottom: 0 !important; +} + +.revision a { + padding: 2px 10px !important; +} + +.revision a span { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.revisionName { + cursor: default; + padding: 1px 10px !important; + max-width: 150px; +} + +.revisionDate { + font-size: 8px; + color: DarkGrey; + display: block; +} + +.revisionName:hover, +.revisionName:focus, +.revisionName:active { + background: transparent; + border-color: #ccc; +} + +.caretSeparator { + height: 22px; + line-height: 9px; + width: 16px; + padding-left: 3px !important; +} + .paragraph-col { margin: 0; padding: 0; diff --git a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js index 6c9eeeec855..76571374998 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js +++ b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js @@ -109,6 +109,8 @@ angular.module('zeppelinWebApp').factory('websocketEvents', $rootScope.$broadcast('appLoad', data); } else if (op === 'APP_STATUS_CHANGE') { $rootScope.$broadcast('appStatusChange', data); + } else if (op === 'LIST_REVISION_HISTORY') { + $rootScope.$broadcast('listRevisionHistory', data); } else if (op === 'NOTE_REVISION') { $rootScope.$broadcast('noteRevision', data); } diff --git a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js index 9a82c9b0590..a4f7802d348 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js +++ b/zeppelin-web/src/components/websocketEvents/websocketMsg.service.js @@ -161,6 +161,15 @@ angular.module('zeppelinWebApp').service('websocketMsgSrv', function($rootScope, }); }, + listRevisionHistory: function(noteId) { + websocketEvents.sendNewEvent({ + op: 'LIST_REVISION_HISTORY', + data: { + noteId: noteId + } + }); + }, + getNoteRevision: function(noteId, revisionId) { websocketEvents.sendNewEvent({ op: 'NOTE_REVISION', diff --git a/zeppelin-web/test/spec/controllers/notebook.js b/zeppelin-web/test/spec/controllers/notebook.js index 317a905b4f1..f4f6e46dbe0 100644 --- a/zeppelin-web/test/spec/controllers/notebook.js +++ b/zeppelin-web/test/spec/controllers/notebook.js @@ -7,7 +7,8 @@ describe('Controller: NotebookCtrl', function() { var scope; var websocketMsgSrvMock = { - getNotebook: function() {} + getNotebook: function() {}, + listRevisionHistory: function() {} }; var baseUrlSrvMock = { 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 9937d616036..3620464b33b 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 @@ -353,9 +353,14 @@ public void removeNote(String id, AuthenticationInfo subject) { } } - public void checkpointNote(String noteId, String checkpointMessage, AuthenticationInfo subject) - throws IOException { - notebookRepo.checkpoint(noteId, checkpointMessage, subject); + public Revision checkpointNote(String noteId, String checkpointMessage, + AuthenticationInfo subject) throws IOException { + return notebookRepo.checkpoint(noteId, checkpointMessage, subject); + } + + public List listRevisionHistory(String noteId, + AuthenticationInfo subject) { + return notebookRepo.revisionHistory(noteId, subject); } public Note getNoteRevision(String noteId, Revision revision, AuthenticationInfo subject) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java index 4e735e802e7..6c499c6e312 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java @@ -21,6 +21,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -366,7 +367,12 @@ public Note get(String noteId, Revision rev, AuthenticationInfo subject) { @Override public List revisionHistory(String noteId, AuthenticationInfo subject) { - // Auto-generated method stub - return null; + List revisions = Collections.emptyList(); + try { + revisions = getRepo(0).revisionHistory(noteId, subject); + } catch (IOException e) { + LOG.error("Failed to list revision history", e); + } + return revisions; } } 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 c0baf403b6a..e2f5fe9186f 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 @@ -114,6 +114,9 @@ public static enum OP { CHECKPOINT_NOTEBOOK, // [c-s] checkpoint notebook to storage repository // @param noteId // @param checkpointName + + LIST_REVISION_HISTORY, // [c-s] list revision history of the notebook + // @param noteId NOTE_REVISION, // [c-s] get certain revision of note // @param noteId // @param revisionId diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java index 8e787a2fdc5..b1d4b384e01 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/repo/GitNotebookRepoTest.java @@ -46,6 +46,7 @@ public class GitNotebookRepoTest { private static final String TEST_NOTE_ID = "2A94M5J1Z"; + private static final String TEST_NOTE_ID2 = "2A94M5J2Z"; private File zeppelinDir; private String notebooksDir; @@ -54,7 +55,7 @@ public class GitNotebookRepoTest { @Before public void setUp() throws Exception { - String zpath = System.getProperty("java.io.tmpdir")+"/ZeppelinTest_"+System.currentTimeMillis(); + String zpath = System.getProperty("java.io.tmpdir") + "/ZeppelinTest_" + System.currentTimeMillis(); zeppelinDir = new File(zpath); zeppelinDir.mkdirs(); new File(zeppelinDir, "conf").mkdirs(); @@ -64,8 +65,11 @@ public void setUp() throws Exception { notebookDir.mkdirs(); String testNoteDir = Joiner.on(File.separator).join(notebooksDir, TEST_NOTE_ID); + String testNoteDir2 = Joiner.on(File.separator).join(notebooksDir, TEST_NOTE_ID2); FileUtils.copyDirectory(new File(Joiner.on(File.separator).join("src", "test", "resources", TEST_NOTE_ID)), - new File(testNoteDir) + new File(testNoteDir)); + FileUtils.copyDirectory(new File(Joiner.on(File.separator).join("src", "test", "resources", TEST_NOTE_ID2)), + new File(testNoteDir2) ); System.setProperty(ConfVars.ZEPPELIN_HOME.getVarName(), zeppelinDir.getAbsolutePath()); @@ -106,7 +110,7 @@ public void initNonemptyNotebookDir() throws IOException, GitAPIException { } @Test - public void showNotebookHistory() throws GitAPIException, IOException { + public void showNotebookHistoryEmptyTest() throws GitAPIException, IOException { //given notebookRepo = new GitNotebookRepo(conf); assertThat(notebookRepo.list(null)).isNotEmpty(); @@ -120,7 +124,50 @@ public void showNotebookHistory() throws GitAPIException, IOException { } @Test - public void addCheckpoint() throws IOException { + public void showNotebookHistoryMultipleNotesTest() throws IOException { + //initial checks + notebookRepo = new GitNotebookRepo(conf); + assertThat(notebookRepo.list(null)).isNotEmpty(); + assertThat(containsNote(notebookRepo.list(null), TEST_NOTE_ID)).isTrue(); + assertThat(containsNote(notebookRepo.list(null), TEST_NOTE_ID2)).isTrue(); + assertThat(notebookRepo.revisionHistory(TEST_NOTE_ID, null)).isEmpty(); + assertThat(notebookRepo.revisionHistory(TEST_NOTE_ID2, null)).isEmpty(); + + //add commit to both notes + notebookRepo.checkpoint(TEST_NOTE_ID, "first commit, note1", null); + assertThat(notebookRepo.revisionHistory(TEST_NOTE_ID, null).size()).isEqualTo(1); + notebookRepo.checkpoint(TEST_NOTE_ID2, "first commit, note2", null); + assertThat(notebookRepo.revisionHistory(TEST_NOTE_ID2, null).size()).isEqualTo(1); + + //modify, save and checkpoint first note + Note note = notebookRepo.get(TEST_NOTE_ID, null); + Paragraph p = note.addParagraph(); + Map config = p.getConfig(); + config.put("enabled", true); + p.setConfig(config); + p.setText("%md note1 test text"); + notebookRepo.save(note, null); + assertThat(notebookRepo.checkpoint(TEST_NOTE_ID, "second commit, note1", null)).isNotNull(); + assertThat(notebookRepo.revisionHistory(TEST_NOTE_ID, null).size()).isEqualTo(2); + assertThat(notebookRepo.revisionHistory(TEST_NOTE_ID2, null).size()).isEqualTo(1); + assertThat(notebookRepo.checkpoint(TEST_NOTE_ID2, "first commit, note2", null)).isNull(); + assertThat(notebookRepo.revisionHistory(TEST_NOTE_ID2, null).size()).isEqualTo(1); + + //modify, save and checkpoint second note + note = notebookRepo.get(TEST_NOTE_ID2, null); + p = note.addParagraph(); + config = p.getConfig(); + config.put("enabled", false); + p.setConfig(config); + p.setText("%md note2 test text"); + notebookRepo.save(note, null); + assertThat(notebookRepo.checkpoint(TEST_NOTE_ID2, "second commit, note2", null)).isNotNull(); + assertThat(notebookRepo.revisionHistory(TEST_NOTE_ID, null).size()).isEqualTo(2); + assertThat(notebookRepo.revisionHistory(TEST_NOTE_ID2, null).size()).isEqualTo(2); + } + + @Test + public void addCheckpointTest() throws IOException { // initial checks notebookRepo = new GitNotebookRepo(conf); assertThat(notebookRepo.list(null)).isNotEmpty(); diff --git a/zeppelin-zengine/src/test/resources/2A94M5J2Z/note.json b/zeppelin-zengine/src/test/resources/2A94M5J2Z/note.json new file mode 100644 index 00000000000..79fe35cada9 --- /dev/null +++ b/zeppelin-zengine/src/test/resources/2A94M5J2Z/note.json @@ -0,0 +1,87 @@ +{ + "paragraphs": [ + { + "text": "%md\n## Congratulations, it\u0027s done.\n##### You can create your own notebook in \u0027Notebook\u0027 menu. Good luck!", + "config": { + "colWidth": 12.0, + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": false, + "keys": [], + "values": [], + "groups": [], + "scatter": {} + }, + "editorHide": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "jobName": "paragraph_1423836268492_216498320", + "id": "20150213-230428_1231780373", + "result": { + "code": "SUCCESS", + "type": "HTML", + "msg": "\u003ch2\u003eCongratulations, it\u0027s done.\u003c/h2\u003e\n\u003ch5\u003eYou can create your own notebook in \u0027Notebook\u0027 menu. Good luck!\u003c/h5\u003e\n" + }, + "dateCreated": "Feb 13, 2015 11:04:28 PM", + "dateStarted": "Apr 1, 2015 9:12:18 PM", + "dateFinished": "Apr 1, 2015 9:12:18 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "text": "%md\n\nAbout bank data\n\n```\nCitation Request:\n This dataset is public available for research. The details are described in [Moro et al., 2011]. \n Please include this citation if you plan to use this database:\n\n [Moro et al., 2011] S. Moro, R. Laureano and P. Cortez. Using Data Mining for Bank Direct Marketing: An Application of the CRISP-DM Methodology. \n In P. Novais et al. (Eds.), Proceedings of the European Simulation and Modelling Conference - ESM\u00272011, pp. 117-121, GuimarĂ£es, Portugal, October, 2011. EUROSIS.\n\n Available at: [pdf] http://hdl.handle.net/1822/14838\n [bib] http://www3.dsi.uminho.pt/pcortez/bib/2011-esm-1.txt\n```", + "config": { + "colWidth": 12.0, + "graph": { + "mode": "table", + "height": 300.0, + "optionOpen": false, + "keys": [], + "values": [], + "groups": [], + "scatter": {} + }, + "editorHide": true + }, + "settings": { + "params": {}, + "forms": {} + }, + "jobName": "paragraph_1427420818407_872443482", + "id": "20150326-214658_12335843", + "result": { + "code": "SUCCESS", + "type": "HTML", + "msg": "\u003cp\u003eAbout bank data\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eCitation Request:\n This dataset is public available for research. The details are described in [Moro et al., 2011]. \n Please include this citation if you plan to use this database:\n\n [Moro et al., 2011] S. Moro, R. Laureano and P. Cortez. Using Data Mining for Bank Direct Marketing: An Application of the CRISP-DM Methodology. \n In P. Novais et al. (Eds.), Proceedings of the European Simulation and Modelling Conference - ESM\u00272011, pp. 117-121, GuimarĂ£es, Portugal, October, 2011. EUROSIS.\n\n Available at: [pdf] http://hdl.handle.net/1822/14838\n [bib] http://www3.dsi.uminho.pt/pcortez/bib/2011-esm-1.txt\n\u003c/code\u003e\u003c/pre\u003e\n" + }, + "dateCreated": "Mar 26, 2015 9:46:58 PM", + "dateStarted": "Jul 3, 2015 1:44:56 PM", + "dateFinished": "Jul 3, 2015 1:44:56 PM", + "status": "FINISHED", + "progressUpdateIntervalMs": 500 + }, + { + "config": {}, + "settings": { + "params": {}, + "forms": {} + }, + "jobName": "paragraph_1435955447812_-158639899", + "id": "20150703-133047_853701097", + "dateCreated": "Jul 3, 2015 1:30:47 PM", + "status": "READY", + "progressUpdateIntervalMs": 500 + } + ], + "name": "Sample note - excerpt from Zeppelin Tutorial", + "id": "2A94M5J2Z", + "angularObjects": {}, + "config": { + "looknfeel": "default" + }, + "info": {} +} \ No newline at end of file