[ZEPPELIN-2813] revisions comparator for note

This commit is contained in:
Tinkoff DWH 2017-07-27 17:31:07 +05:00
parent 2e59008089
commit a192b95dbb
12 changed files with 406 additions and 11 deletions

View file

@ -29,8 +29,6 @@ import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
@ -50,8 +48,8 @@ import org.apache.zeppelin.interpreter.InterpreterSetting;
import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry;
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.JobListenerFactory;
import org.apache.zeppelin.notebook.Note;
import org.apache.zeppelin.notebook.Notebook;
import org.apache.zeppelin.notebook.NotebookAuthorization;
@ -59,7 +57,6 @@ import org.apache.zeppelin.notebook.NotebookEventListener;
import org.apache.zeppelin.notebook.NotebookImportDeserializer;
import org.apache.zeppelin.notebook.Paragraph;
import org.apache.zeppelin.notebook.ParagraphJobListener;
import org.apache.zeppelin.notebook.ParagraphRuntimeInfo;
import org.apache.zeppelin.notebook.repo.NotebookRepo.Revision;
import org.apache.zeppelin.notebook.socket.Message;
import org.apache.zeppelin.notebook.socket.Message.OP;
@ -83,11 +80,11 @@ import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.google.common.collect.Queues;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
/**
@ -340,6 +337,9 @@ public class NotebookServer extends WebSocketServlet
case NOTE_REVISION:
getNoteByRevision(conn, notebook, messagereceived);
break;
case NOTE_REVISION_FOR_COMPARE:
getNoteByRevisionForCompare(conn, notebook, messagereceived);
break;
case LIST_NOTE_JOBS:
unicastNoteJobInfo(conn, messagereceived);
break;
@ -1920,6 +1920,25 @@ public class NotebookServer extends WebSocketServlet
.put("note", revisionNote)));
}
private void getNoteByRevisionForCompare(NotebookSocket conn, Notebook notebook,
Message fromMessage) throws IOException {
String noteId = (String) fromMessage.get("noteId");
String revisionId = (String) fromMessage.get("revisionId");
String position = (String) fromMessage.get("position");
AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
Note revisionNote;
if (revisionId.equals("Head")) {
revisionNote = notebook.loadNoteFromRepo(noteId, subject);
} else {
revisionNote = notebook.getNoteByRevision(noteId, revisionId, subject);
}
conn.send(serializeMessage(
new Message(OP.NOTE_REVISION_FOR_COMPARE).put("noteId", noteId)
.put("revisionId", revisionId).put("position", position).put("note", revisionNote)));
}
/**
* This callback is for the paragraph that runs on ZeppelinServer
*

View file

@ -32,7 +32,8 @@
"bootstrap3-dialog": "bootstrap-dialog#~1.34.7",
"select2": "^4.0.3",
"MathJax": "2.7.0",
"ngclipboard": "^1.1.1"
"ngclipboard": "^1.1.1",
"jsdiff": "3.3.0"
},
"devDependencies": {
"angular-mocks": "1.5.7"

View file

@ -86,6 +86,7 @@ module.exports = function(config) {
'bower_components/MathJax/MathJax.js',
'bower_components/clipboard/dist/clipboard.js',
'bower_components/ngclipboard/dist/ngclipboard.js',
'bower_components/jsdiff/diff.js',
'bower_components/angular-mocks/angular-mocks.js',
// endbower

View file

@ -139,6 +139,15 @@ limitations under the License.
</div>
</li>
</ul>
<button type="button"
ng-if="noteRevisions.length > 1"
class="btn btn-default btn-xs"
id="compareRevisions"
data-toggle="modal"
data-target="#revisionsComparatorModal"
tooltip-placement="bottom" uib-tooltip="Compare revisions">
<i class="fa fa-exchange"></i>
</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default btn-xs revisionName" title="{{currentRevision}}">

View file

@ -59,6 +59,11 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope,
}
$scope.noteRevisions = []
$scope.firstNoteRevisionForCompare = null
$scope.secondNoteRevisionForCompare = null
$scope.mergeNoteRevisionsForCompare = null
$scope.currentFirstRevisionForCompare = 'Choose...'
$scope.currentSecondRevisionForCompare = 'Choose...'
$scope.currentRevision = 'Head'
$scope.revisionView = isRevisionPath($location.path())
@ -231,13 +236,24 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope,
})
}
$scope.preVisibleRevisionsComparator = function() {
$scope.mergeNoteRevisionsForCompare = null
$scope.firstNoteRevisionForCompare = null
$scope.secondNoteRevisionForCompare = null
$scope.currentFirstRevisionForCompare = 'Choose...'
$scope.currentSecondRevisionForCompare = 'Choose...'
$scope.$apply()
}
$scope.$on('listRevisionHistory', function (event, data) {
console.debug('received list of revisions %o', data)
$scope.noteRevisions = data.revisionList
$scope.noteRevisions.splice(0, 0, {
id: 'Head',
message: 'Head'
})
if ($scope.noteRevisions.length === 0 || $scope.noteRevisions[0].id !== 'Head') {
$scope.noteRevisions.splice(0, 0, {
id: 'Head',
message: 'Head'
})
}
if ($routeParams.revisionId) {
let index = _.findIndex($scope.noteRevisions, {'id': $routeParams.revisionId})
if (index > -1) {
@ -278,6 +294,99 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope,
}
}
// compare revisions
$scope.compareRevisions = function () {
if ($scope.firstNoteRevisionForCompare && $scope.secondNoteRevisionForCompare) {
let paragraphs1 = $scope.firstNoteRevisionForCompare.note.paragraphs
let paragraphs2 = $scope.secondNoteRevisionForCompare.note.paragraphs
let merge = {
added: [],
deleted: [],
compared: []
}
for (let p1 of paragraphs1) {
let p2 = null
for (let p of paragraphs2) {
if (p1.id === p.id) {
p2 = p
break
}
}
if (p2 === null) {
merge.deleted.push({paragraph: p1, firstString: (p1.text || '').split('\n')[0]})
} else {
let colorClass = ''
let span = null
let text1 = p1.text || ''
let text2 = p2.text || ''
let diff = window.JsDiff.diffLines(text1, text2)
let diffHtml = document.createDocumentFragment()
let identical = true
let identicalClass = 'color-black'
diff.forEach(function(part) {
colorClass = part.added ? 'color-green' : part.removed ? 'color-red' : identicalClass
span = document.createElement('span')
span.className = colorClass
if (identical && colorClass !== identicalClass) {
identical = false
}
span.appendChild(document.createTextNode(part.value))
diffHtml.appendChild(span)
})
let pre = document.createElement('pre')
pre.appendChild(diffHtml)
merge.compared.push(
{paragraph: p1, diff: pre.innerHTML, identical: identical, firstString: (p1.text || '').split('\n')[0]})
}
}
for (let p2 of paragraphs2) {
let p1 = null
for (let p of paragraphs1) {
if (p2.id === p.id) {
p1 = p
break
}
}
if (p1 === null) {
merge.added.push({paragraph: p2, firstString: (p2.text || '').split('\n')[0]})
}
}
$scope.mergeNoteRevisionsForCompare = merge
}
}
$scope.getNoteRevisionForReview = function (revision, position) {
if (position) {
if (position === 'first') {
$scope.currentFirstRevisionForCompare = revision.message
} else {
$scope.currentSecondRevisionForCompare = revision.message
}
websocketMsgSrv.getNoteByRevisionForCompare($routeParams.noteId, revision.id, position)
}
}
$scope.$on('noteRevisionForCompare', function (event, data) {
console.debug('received note revision for compare %o', data)
if (data.note && data.position) {
if (data.position === 'first') {
$scope.firstNoteRevisionForCompare = data
} else {
$scope.secondNoteRevisionForCompare = data
}
if ($scope.firstNoteRevisionForCompare !== null && $scope.secondNoteRevisionForCompare !== null &&
$scope.firstNoteRevisionForCompare.revisionId !== $scope.secondNoteRevisionForCompare.revisionId) {
$scope.compareRevisions()
}
}
})
$scope.runAllParagraphs = function (noteId) {
BootstrapDialog.confirm({
closable: true,

View file

@ -13,6 +13,7 @@ limitations under the License.
-->
<!-- Here the controller <NotebookCtrl> is not needed because explicitly set in the app.js (route) -->
<div id="actionbar" ng-include src="'app/notebook/notebook-actionBar.html'"></div>
<div id="note-revisions-comparator-modal-container" ng-include src="'app/notebook/revisions-comparator.html'"></div>
<div id="content" class="notebookContent">
<!-- settings -->
<div ng-if="showSetting" class="setting">

View file

@ -270,6 +270,117 @@ table.table-shortcut {
font-size: 10px !important;
}
.revisions-comparator-modal-dialog {
width: 80%;
margin: 30px auto;
height: 80%;
}
.revisions-comparator-modal-dialog .modal-content {
height: 100%;
}
.revisions-comparator-modal-header {
min-height: 16.428571429px;
padding: 15px;
border-bottom: 1px solid #9cb4c5;
background-color: #3071a9;
border: 2px solid #3071a9;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.revisions-comparator-modal-header .close {
color: #cfcfcf;
opacity: 1;
}
.revisions-comparator-modal-title {
color: white;
margin-top: 2px;
margin-bottom: 2px;
}
.revisions-comparator-code-panel {
display: inline-block;
width: 50%;
float: left;
height: 300px;
overflow-y: auto;
max-height: 300px
}
.revisions-comparator-code-panel-title {
width: 50%;
float: left;
font-size: 14px;
padding: 5px;
background-color: #f5f5f5;
}
.revisions-comparator-bar {
margin: 10px auto;
width: 400px;
}
.revisions-comparator-status {
font-size: 12px;
padding-left: 10px;
}
#diffPanel {
height: calc(100% - 60px);
}
#diffPanel .panel-group {
height: inherit;
overflow: auto;
}
.revision-name-for-compare {
cursor: default;
overflow: hidden;
display: inline-block;
max-width: 100px;
width: 100px;
padding: 1px 10px;
}
.revisions-comparator-caret {
margin: 0 0 10px 6px;
}
.revisions-comparator-link, .revisions-comparator-link:hover,
.revisions-comparator-link:visited, .revisions-comparator-link:focus {
text-decoration: none;
color: #000;
}
.revisions-comparator-forst-string {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 12px;
color: grey;
}
.color-green {
color: green;
}
.color-red {
color: red;
}
.color-black {
color: black;
}
.color-orange {
color: orange;
}
/*
Paragraph Title
*/

View file

@ -0,0 +1,126 @@
<!--
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
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.
-->
<div class="modal fade" id="revisionsComparatorModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"
modalvisible previsiblecallback="preVisibleRevisionsComparator">
<div class="revisions-comparator-modal-dialog">
<div class="modal-content">
<div class="revisions-comparator-modal-header">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
<h4 class="revisions-comparator-modal-title" id="myModalLabel">Revisions comparator</h4>
</div>
<div class="revisions-comparator-bar">
<div class="btn-group">
<button type="button" ng-if="noteRevisions.length > 0"
class="btn btn-sm btn-default dropdown-toggle"
data-toggle="dropdown" id="firstRevisionDropdown" title="{{currentFirstRevisionForCompare}}">
<div class="revision-name-for-compare">{{currentFirstRevisionForCompare}}</div>
<span class="caret revisions-comparator-caret"></span>
</button>
<ul class="dropdown-menu pull-right" aria-labelledby="firstRevisionDropdown">
<li></li>
<li ng-repeat="revision in noteRevisions | orderBy:'time':true" class="revision">
<a style="cursor:pointer" ng-click="getNoteRevisionForReview(revision, 'first')">
<span style="display: block;">
<strong>{{revision.message}}</strong>
</span>
<span class="revisionDate">
<em>{{formatRevisionDate(revision.time)}}</em>
</span>
</a>
</li>
</ul>
</div>
<span>compare with</span>
<div class="btn-group">
<button type="button" ng-if="noteRevisions.length > 0"
class="btn btn-sm btn-default dropdown-toggle"
ng-disabled="firstNoteRevisionForCompare === null"
data-toggle="dropdown" id="secondRevisionDropdown" title="{{currentSecondRevisionForCompare}}">
<div class="revision-name-for-compare">{{currentSecondRevisionForCompare}}</div>
<span class="caret revisions-comparator-caret"></span>
</button>
<ul class="dropdown-menu pull-right" aria-labelledby="secondRevisionDropdown">
<li ng-repeat="revision in noteRevisions | orderBy:'time':true" class="revision">
<a style="cursor:pointer" ng-click="getNoteRevisionForReview(revision, 'second')">
<span style="display: block;">
<strong>{{revision.message}}</strong>
</span>
<span class="revisionDate">
<em>{{formatRevisionDate(revision.time)}}</em>
</span>
</a>
</li>
</ul>
</div>
</div>
<div id="diffPanel">
<div class="panel-group">
<div class="panel" data-ng-repeat="p in (mergeNoteRevisionsForCompare ? mergeNoteRevisionsForCompare.added : [])">
<div class="panel-heading">
<a class="revisions-comparator-link" data-toggle="collapse" data-parent="#diffPanel" href="#{{p.paragraph.id}}">
<h4 class="panel-title">
{{p.paragraph.id}}<strong style="padding: 5px;" ng-if="p.paragraph.title">({{p.paragraph.title}})</strong>
<i class="revisions-comparator-status color-green">added</i>
<i class="revisions-comparator-forst-string">{{p.firstString}}</i>
</h4>
</a>
</div>
<div id="{{p.paragraph.id}}" class="panel-collapse collapse">
<span class="revisions-comparator-code-panel-title">Revision: <strong>{{currentFirstRevisionForCompare}}</strong></span>
<span class="revisions-comparator-code-panel-title">Revision: <strong>{{currentSecondRevisionForCompare}}</strong></span>
<pre class="revisions-comparator-code-panel"></pre>
<pre class="revisions-comparator-code-panel color-green">{{p.paragraph.text}}</pre>
</div>
</div>
<div class="panel" data-ng-repeat="p in (mergeNoteRevisionsForCompare ? mergeNoteRevisionsForCompare.deleted : [])">
<div class="panel-heading">
<a class="revisions-comparator-link" data-toggle="collapse" data-parent="#diffPanel" href="#{{p.paragraph.id}}">
<h4 class="panel-title">
{{p.paragraph.id}} <strong style="padding: 5px;" ng-if="p.paragraph.title">({{p.paragraph.title}})</strong>
<i class="revisions-comparator-status color-red">deleted</i>
<i class="revisions-comparator-forst-string">{{p.firstString}}</i>
</h4>
</a>
</div>
<div id="{{p.paragraph.id}}" class="panel-collapse collapse">
<span class="revisions-comparator-code-panel-title">Revision: <strong>{{currentFirstRevisionForCompare}}</strong></span>
<span class="revisions-comparator-code-panel-title">Revision: <strong>{{currentSecondRevisionForCompare}}</strong></span>
<pre class="revisions-comparator-code-panel color-red">{{p.paragraph.text}}</pre>
<pre class="revisions-comparator-code-panel"></pre>
</div>
</div>
<div class="panel" data-ng-repeat="p in (mergeNoteRevisionsForCompare ? mergeNoteRevisionsForCompare.compared : [])">
<div class="panel-heading">
<a class="revisions-comparator-link" data-toggle="collapse" data-parent="#diffPanel" href="#{{p.paragraph.id}}">
<h4 class="panel-title">
{{p.paragraph.id}} <strong style="padding: 5px;" ng-if="p.paragraph.title">({{p.paragraph.title}})</strong>
<i class="revisions-comparator-status" ng-show="p.identical">contents are identical</i>
<i class="revisions-comparator-status color-orange" ng-show="!(p.identical)">there are differences</i>
<i class="revisions-comparator-forst-string">{{p.firstString}}</i>
</h4>
</a>
</div>
<div id="{{p.paragraph.id}}" class="panel-collapse collapse">
<span class="revisions-comparator-code-panel-title">Revision: <strong>{{currentFirstRevisionForCompare}}</strong></span>
<span class="revisions-comparator-code-panel-title">Diff with revision: <strong>{{currentSecondRevisionForCompare}}</strong></span>
<pre class="revisions-comparator-code-panel">{{p.paragraph.text}}</pre>
<pre class="revisions-comparator-code-panel" ng-bind-html="p.diff"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -138,6 +138,8 @@ function WebsocketEventFactory ($rootScope, $websocket, $location, baseUrlSrv) {
$rootScope.$broadcast('listRevisionHistory', data)
} else if (op === 'NOTE_REVISION') {
$rootScope.$broadcast('noteRevision', data)
} else if (op === 'NOTE_REVISION_FOR_COMPARE') {
$rootScope.$broadcast('noteRevisionForCompare', data)
} else if (op === 'INTERPRETER_BINDINGS') {
$rootScope.$broadcast('interpreterBindings', data)
} else if (op === 'ERROR_INFO') {

View file

@ -294,6 +294,17 @@ function WebsocketMessageService ($rootScope, websocketEvents) {
})
},
getNoteByRevisionForCompare: function (noteId, revisionId, position) {
websocketEvents.sendNewEvent({
op: 'NOTE_REVISION_FOR_COMPARE',
data: {
noteId: noteId,
revisionId: revisionId,
position: position
}
})
},
getEditorSetting: function (paragraphId, replName) {
websocketEvents.sendNewEvent({
op: 'EDITOR_SETTING',

View file

@ -165,6 +165,7 @@ limitations under the License.
<script src="bower_components/MathJax/MathJax.js"></script>
<script src="bower_components/clipboard/dist/clipboard.js"></script>
<script src="bower_components/ngclipboard/dist/ngclipboard.js"></script>
<script src="bower_components/jsdiff/diff.js"></script>
<!-- endbower -->
<!-- endbuild -->
</body>

View file

@ -152,6 +152,10 @@ public class Message implements JsonSerializable {
SET_NOTE_REVISION, // [c-s] set current notebook head to this revision
// @param noteId
// @param revisionId
NOTE_REVISION_FOR_COMPARE, // [c-s] get certain revision of note for compare
// @param noteId
// @param revisionId
// @param position
APP_APPEND_OUTPUT, // [s-c] append output
APP_UPDATE_OUTPUT, // [s-c] update (replace) output
APP_LOAD, // [s-c] on app load