Support of aggregation results
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 119 KiB |
0
docs/assets/themes/zeppelin/img/docs-img/elasticsearch-count-with-query.png
Executable file → Normal file
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
0
docs/assets/themes/zeppelin/img/docs-img/elasticsearch-query-string.png
Executable file → Normal file
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
0
docs/assets/themes/zeppelin/img/docs-img/elasticsearch-search-json-query-table.png
Executable file → Normal file
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
|
@ -110,6 +110,9 @@ With the `search` command, you can send a search query to Elasticsearch. There a
|
|||
* This is a shortcut to a query like that: `{ "query": { "query_string": { "query": "__HERE YOUR QUERY__", "analyze_wildcard": true } } }`
|
||||
* See [Elasticsearch query string syntax](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax) for more details about the content of such a query.
|
||||
|
||||
A search query can also contain [aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html). If there is at least one aggregation, the result of the first aggregation is shown, otherwise, you get the search hits.
|
||||
|
||||
|
||||
```bash
|
||||
| %elasticsearch
|
||||
| search /index1,index2,.../type1,type2,... <JSON document containing the query or query_string elements>
|
||||
|
|
@ -134,6 +137,15 @@ Examples:
|
|||
|
|
||||
| %elasticsearch
|
||||
| search /logs { "query": { "query_string": { "query": "request.method:GET AND status:200" } } }
|
||||
|
|
||||
| %elasticsearch
|
||||
| search /logs { "aggs": {
|
||||
| "content_length_stats": {
|
||||
| "extended_stats": {
|
||||
| "field": "content_length"
|
||||
| }
|
||||
| }
|
||||
| } }
|
||||
```
|
||||
|
||||
* With query_string elements:
|
||||
|
|
@ -159,16 +171,17 @@ Suppose we have a JSON document:
|
|||
"url": "/zeppelin/4cd001cd-c517-4fa9-b8e5-a06b8f4056c4",
|
||||
"headers": [ "Accept: *.*", "Host: apache.org"]
|
||||
},
|
||||
"status": "403"
|
||||
"status": "403",
|
||||
"content_length": 1234
|
||||
}
|
||||
```
|
||||
|
||||
The data will be flattened like this:
|
||||
|
||||
|
||||
date | request.headers[0] | request.headers[1] | request.method | request.url | status
|
||||
-----|--------------------|--------------------|----------------|-------------|-------
|
||||
2015-12-08T21:03:13.588Z | Accept: \*.\* | Host: apache.org | GET | /zeppelin/4cd001cd-c517-4fa9-b8e5-a06b8f4056c4 | 403
|
||||
content_length | date | request.headers[0] | request.headers[1] | request.method | request.url | status
|
||||
---------------|------|--------------------|--------------------|----------------|-------------|-------
|
||||
1234 | 2015-12-08T21:03:13.588Z | Accept: \*.\* | Host: apache.org | GET | /zeppelin/4cd001cd-c517-4fa9-b8e5-a06b8f4056c4 | 403
|
||||
|
||||
|
||||
Examples:
|
||||
|
|
@ -185,6 +198,12 @@ Examples:
|
|||
* With a query string:
|
||||

|
||||
|
||||
* With a query containing a multi-value metric aggregation:
|
||||

|
||||
|
||||
* With a query containing a multi-bucket aggregation:
|
||||

|
||||
|
||||
|
||||
#### count
|
||||
With the `count` command, you can count documents available in some indices and types. You can also provide a query.
|
||||
|
|
|
|||
|
|
@ -17,17 +17,10 @@
|
|||
|
||||
package org.apache.zeppelin.elasticsearch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import com.github.wnameless.json.flattener.JsonFlattener;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.zeppelin.interpreter.Interpreter;
|
||||
import org.apache.zeppelin.interpreter.InterpreterContext;
|
||||
|
|
@ -43,16 +36,21 @@ import org.elasticsearch.client.Client;
|
|||
import org.elasticsearch.client.transport.TransportClient;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.transport.InetSocketTransportAddress;
|
||||
import org.elasticsearch.common.xcontent.XContentHelper;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.search.SearchHit;
|
||||
import org.elasticsearch.search.aggregations.Aggregation;
|
||||
import org.elasticsearch.search.aggregations.Aggregations;
|
||||
import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation;
|
||||
import org.elasticsearch.search.aggregations.bucket.InternalSingleBucketAggregation;
|
||||
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
|
||||
import org.elasticsearch.search.aggregations.metrics.InternalMetricsAggregation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.github.wnameless.json.flattener.JsonFlattener;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -313,7 +311,8 @@ public class ElasticsearchInterpreter extends Interpreter {
|
|||
* Processes a "search" request.
|
||||
*
|
||||
* @param urlItems Items of the URL
|
||||
* @param data May contains the limit and the JSON of the request
|
||||
* @param data May contains the JSON of the request
|
||||
* @param size Limit of result set
|
||||
* @return Result of the search request, it contains a tab-formatted string of the matching hits
|
||||
*/
|
||||
private InterpreterResult processSearch(String[] urlItems, String data, int size) {
|
||||
|
|
@ -325,10 +324,7 @@ public class ElasticsearchInterpreter extends Interpreter {
|
|||
|
||||
final SearchResponse response = searchData(urlItems, data, size);
|
||||
|
||||
return new InterpreterResult(
|
||||
InterpreterResult.Code.SUCCESS,
|
||||
InterpreterResult.Type.TABLE,
|
||||
buildResponseMessage(response.getHits().getHits()));
|
||||
return buildResponseMessage(response);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -419,7 +415,39 @@ public class ElasticsearchInterpreter extends Interpreter {
|
|||
return response;
|
||||
}
|
||||
|
||||
private String buildResponseMessage(SearchHit[] hits) {
|
||||
private InterpreterResult buildAggResponseMessage(Aggregations aggregations) {
|
||||
|
||||
// Only the result of the first aggregation is returned
|
||||
//
|
||||
final Aggregation agg = aggregations.asList().get(0);
|
||||
InterpreterResult.Type resType = InterpreterResult.Type.TEXT;
|
||||
String resMsg = "";
|
||||
|
||||
if (agg instanceof InternalMetricsAggregation) {
|
||||
resMsg = XContentHelper.toString((InternalMetricsAggregation) agg).toString();
|
||||
}
|
||||
else if (agg instanceof InternalSingleBucketAggregation) {
|
||||
resMsg = XContentHelper.toString((InternalSingleBucketAggregation) agg).toString();
|
||||
}
|
||||
else if (agg instanceof InternalMultiBucketAggregation) {
|
||||
final StringBuffer buffer = new StringBuffer("key\tdoc_count");
|
||||
|
||||
final InternalMultiBucketAggregation multiBucketAgg = (InternalMultiBucketAggregation) agg;
|
||||
for (MultiBucketsAggregation.Bucket bucket : multiBucketAgg.getBuckets()) {
|
||||
buffer.append("\n")
|
||||
.append(bucket.getKeyAsString())
|
||||
.append("\t")
|
||||
.append(bucket.getDocCount());
|
||||
}
|
||||
|
||||
resType = InterpreterResult.Type.TABLE;
|
||||
resMsg = buffer.toString();
|
||||
}
|
||||
|
||||
return new InterpreterResult(InterpreterResult.Code.SUCCESS, resType, resMsg);
|
||||
}
|
||||
|
||||
private String buildSearchHitsResponseMessage(SearchHit[] hits) {
|
||||
|
||||
if (hits == null || hits.length == 0) {
|
||||
return "";
|
||||
|
|
@ -462,4 +490,18 @@ public class ElasticsearchInterpreter extends Interpreter {
|
|||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
private InterpreterResult buildResponseMessage(SearchResponse response) {
|
||||
|
||||
final Aggregations aggregations = response.getAggregations();
|
||||
|
||||
if (aggregations != null && aggregations.asList().size() > 0) {
|
||||
return buildAggResponseMessage(aggregations);
|
||||
}
|
||||
|
||||
return new InterpreterResult(
|
||||
InterpreterResult.Code.SUCCESS,
|
||||
InterpreterResult.Type.TABLE,
|
||||
buildSearchHitsResponseMessage(response.getHits().getHits()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,31 +17,27 @@
|
|||
|
||||
package org.apache.zeppelin.elasticsearch;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang.math.RandomUtils;
|
||||
import org.apache.zeppelin.interpreter.InterpreterResult;
|
||||
import org.apache.zeppelin.interpreter.InterpreterResult.Code;
|
||||
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
|
||||
import org.elasticsearch.client.Client;
|
||||
import org.elasticsearch.client.transport.TransportClient;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.transport.InetSocketTransportAddress;
|
||||
import org.elasticsearch.node.Node;
|
||||
import org.elasticsearch.node.NodeBuilder;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class ElasticsearchInterpreterTest {
|
||||
|
||||
private static Client elsClient;
|
||||
|
|
@ -49,7 +45,7 @@ public class ElasticsearchInterpreterTest {
|
|||
private static ElasticsearchInterpreter interpreter;
|
||||
|
||||
private static final String[] METHODS = { "GET", "PUT", "DELETE", "POST" };
|
||||
private static final String[] STATUS = { "200", "404", "500", "403" };
|
||||
private static final int[] STATUS = { 200, 404, 500, 403 };
|
||||
|
||||
private static final String ELS_CLUSTER_NAME = "zeppelin-elasticsearch-interpreter-test";
|
||||
private static final String ELS_HOST = "localhost";
|
||||
|
|
@ -71,6 +67,14 @@ public class ElasticsearchInterpreterTest {
|
|||
|
||||
elsNode = NodeBuilder.nodeBuilder().settings(settings).node();
|
||||
elsClient = elsNode.client();
|
||||
|
||||
elsClient.admin().indices().prepareCreate("logs")
|
||||
.addMapping("http", jsonBuilder()
|
||||
.startObject().startObject("http").startObject("properties")
|
||||
.startObject("content_length")
|
||||
.field("type", "integer")
|
||||
.endObject()
|
||||
.endObject().endObject().endObject()).get();
|
||||
|
||||
for (int i = 0; i < 50; i++) {
|
||||
elsClient.prepareIndex("logs", "http", "" + i)
|
||||
|
|
@ -84,6 +88,7 @@ public class ElasticsearchInterpreterTest {
|
|||
.field("headers", Arrays.asList("Accept: *.*", "Host: apache.org"))
|
||||
.endObject()
|
||||
.field("status", STATUS[RandomUtils.nextInt(STATUS.length)])
|
||||
.field("content_length", RandomUtils.nextInt(2000))
|
||||
)
|
||||
.get();
|
||||
}
|
||||
|
|
@ -147,6 +152,31 @@ public class ElasticsearchInterpreterTest {
|
|||
res = interpreter.interpret("search /logs status:404", null);
|
||||
assertEquals(Code.SUCCESS, res.code());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAgg() {
|
||||
|
||||
// Single-value metric
|
||||
InterpreterResult res = interpreter.interpret("search /logs { \"aggs\" : { \"distinct_status_count\" : " +
|
||||
" { \"cardinality\" : { \"field\" : \"status\" } } } }", null);
|
||||
assertEquals(Code.SUCCESS, res.code());
|
||||
|
||||
// Multi-value metric
|
||||
res = interpreter.interpret("search /logs { \"aggs\" : { \"content_length_stats\" : " +
|
||||
" { \"extended_stats\" : { \"field\" : \"content_length\" } } } }", null);
|
||||
assertEquals(Code.SUCCESS, res.code());
|
||||
|
||||
// Single bucket
|
||||
res = interpreter.interpret("search /logs { \"aggs\" : { " +
|
||||
" \"200_OK\" : { \"filter\" : { \"term\": { \"status\": \"200\" } }, " +
|
||||
" \"aggs\" : { \"avg_length\" : { \"avg\" : { \"field\" : \"content_length\" } } } } } }", null);
|
||||
assertEquals(Code.SUCCESS, res.code());
|
||||
|
||||
// Multi-buckets
|
||||
res = interpreter.interpret("search /logs { \"aggs\" : { \"status_count\" : " +
|
||||
" { \"terms\" : { \"field\" : \"status\" } } } }", null);
|
||||
assertEquals(Code.SUCCESS, res.code());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIndex() {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
|
|||
var currentRoute = $route.current;
|
||||
|
||||
if (currentRoute) {
|
||||
|
||||
setTimeout(
|
||||
function() {
|
||||
var routeParams = currentRoute.params;
|
||||
|
|
@ -91,6 +90,35 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
|
|||
|
||||
initNotebook();
|
||||
|
||||
|
||||
$scope.focusParagraphOnClick = function(clickEvent) {
|
||||
if (!$scope.note) {
|
||||
return;
|
||||
}
|
||||
for (var i=0; i<$scope.note.paragraphs.length; i++) {
|
||||
var paragraphId = $scope.note.paragraphs[i].id;
|
||||
if (jQuery.contains(angular.element('#' + paragraphId + '_container')[0], clickEvent.target)) {
|
||||
$scope.$broadcast('focusParagraph', paragraphId, 0, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// register mouseevent handler for focus paragraph
|
||||
document.addEventListener('click', $scope.focusParagraphOnClick);
|
||||
|
||||
|
||||
$scope.keyboardShortcut = function(keyEvent) {
|
||||
// handle keyevent
|
||||
if (!$scope.viewOnly) {
|
||||
$scope.$broadcast('keyEvent', keyEvent);
|
||||
}
|
||||
};
|
||||
|
||||
// register mouseevent handler for focus paragraph
|
||||
document.addEventListener('keydown', $scope.keyboardShortcut);
|
||||
|
||||
|
||||
/** Remove the note and go back tot he main page */
|
||||
/** TODO(anthony): In the nearly future, go back to the main page and telle to the dude that the note have been remove */
|
||||
$scope.removeNote = function(noteId) {
|
||||
|
|
@ -238,6 +266,9 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
|
|||
angular.element(window).off('beforeunload');
|
||||
$scope.killSaveTimer();
|
||||
$scope.saveNote();
|
||||
|
||||
document.removeEventListener('click', $scope.focusParagraphOnClick);
|
||||
document.removeEventListener('keydown', $scope.keyboardShortcut);
|
||||
});
|
||||
|
||||
$scope.setLookAndFeel = function(looknfeel) {
|
||||
|
|
@ -316,24 +347,6 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
|
|||
return noteCopy;
|
||||
};
|
||||
|
||||
$scope.$on('moveParagraphUp', function(event, paragraphId) {
|
||||
var newIndex = -1;
|
||||
for (var i=0; i<$scope.note.paragraphs.length; i++) {
|
||||
if ($scope.note.paragraphs[i].id === paragraphId) {
|
||||
newIndex = i-1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newIndex<0 || newIndex>=$scope.note.paragraphs.length) {
|
||||
return;
|
||||
}
|
||||
// save dirtyText of moving paragraphs.
|
||||
var prevParagraphId = $scope.note.paragraphs[newIndex].id;
|
||||
angular.element('#' + paragraphId + '_paragraphColumn_main').scope().saveParagraph();
|
||||
angular.element('#' + prevParagraphId + '_paragraphColumn_main').scope().saveParagraph();
|
||||
websocketMsgSrv.moveParagraph(paragraphId, newIndex);
|
||||
});
|
||||
|
||||
// create new paragraph on current position
|
||||
$scope.$on('insertParagraph', function(event, paragraphId, position) {
|
||||
var newIndex = -1;
|
||||
|
|
@ -355,6 +368,24 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
|
|||
websocketMsgSrv.insertParagraph(newIndex);
|
||||
});
|
||||
|
||||
$scope.$on('moveParagraphUp', function(event, paragraphId) {
|
||||
var newIndex = -1;
|
||||
for (var i=0; i<$scope.note.paragraphs.length; i++) {
|
||||
if ($scope.note.paragraphs[i].id === paragraphId) {
|
||||
newIndex = i-1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newIndex<0 || newIndex>=$scope.note.paragraphs.length) {
|
||||
return;
|
||||
}
|
||||
// save dirtyText of moving paragraphs.
|
||||
var prevParagraphId = $scope.note.paragraphs[newIndex].id;
|
||||
angular.element('#' + paragraphId + '_paragraphColumn_main').scope().saveParagraph();
|
||||
angular.element('#' + prevParagraphId + '_paragraphColumn_main').scope().saveParagraph();
|
||||
websocketMsgSrv.moveParagraph(paragraphId, newIndex);
|
||||
});
|
||||
|
||||
$scope.$on('moveParagraphDown', function(event, paragraphId) {
|
||||
var newIndex = -1;
|
||||
for (var i=0; i<$scope.note.paragraphs.length; i++) {
|
||||
|
|
@ -383,11 +414,8 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
|
|||
continue;
|
||||
}
|
||||
} else {
|
||||
var p = $scope.note.paragraphs[i];
|
||||
if (!p.config.hide && !p.config.editorHide) {
|
||||
$scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id, -1);
|
||||
break;
|
||||
}
|
||||
$scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id, -1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -401,11 +429,8 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
|
|||
continue;
|
||||
}
|
||||
} else {
|
||||
var p = $scope.note.paragraphs[i];
|
||||
if (!p.config.hide && !p.config.editorHide) {
|
||||
$scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id, 0);
|
||||
break;
|
||||
}
|
||||
$scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -426,11 +451,22 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
|
|||
var numNewParagraphs = newParagraphIds.length;
|
||||
var numOldParagraphs = oldParagraphIds.length;
|
||||
|
||||
var paragraphToBeFocused;
|
||||
var focusedParagraph;
|
||||
for (var i=0; i<$scope.note.paragraphs.length; i++) {
|
||||
var paragraphId = $scope.note.paragraphs[i].id;
|
||||
if (angular.element('#' + paragraphId + '_paragraphColumn_main').scope().paragraphFocused) {
|
||||
focusedParagraph = paragraphId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** add a new paragraph */
|
||||
if (numNewParagraphs > numOldParagraphs) {
|
||||
for (var index in newParagraphIds) {
|
||||
if (oldParagraphIds[index] !== newParagraphIds[index]) {
|
||||
$scope.note.paragraphs.splice(index, 0, note.paragraphs[index]);
|
||||
paragraphToBeFocused = note.paragraphs[index].id;
|
||||
break;
|
||||
}
|
||||
$scope.$broadcast('updateParagraph', {paragraph: note.paragraphs[index]});
|
||||
|
|
@ -451,6 +487,10 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
|
|||
// rebuild id list since paragraph has moved.
|
||||
oldParagraphIds = $scope.note.paragraphs.map(function(x) {return x.id;});
|
||||
}
|
||||
|
||||
if (focusedParagraph === newParagraphIds[idx]) {
|
||||
paragraphToBeFocused = focusedParagraph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -463,6 +503,13 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// restore focus of paragraph
|
||||
for (var f=0; f<$scope.note.paragraphs.length; f++) {
|
||||
if (paragraphToBeFocused === $scope.note.paragraphs[f].id) {
|
||||
$scope.note.paragraphs[f].focus = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var getInterpreterBindings = function(callback) {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ angular.module('zeppelinWebApp')
|
|||
$scope.colWidthOption = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
|
||||
$scope.showTitleEditor = false;
|
||||
$scope.paragraphFocused = false;
|
||||
if (newParagraph.focus) {
|
||||
$scope.paragraphFocused = true;
|
||||
}
|
||||
|
||||
if (!$scope.paragraph.config) {
|
||||
$scope.paragraph.config = {};
|
||||
|
|
@ -244,6 +247,7 @@ angular.module('zeppelinWebApp')
|
|||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -284,6 +288,15 @@ angular.module('zeppelinWebApp')
|
|||
commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams);
|
||||
};
|
||||
|
||||
$scope.run = function() {
|
||||
var editorValue = $scope.editor.getValue();
|
||||
if (editorValue) {
|
||||
if (!($scope.paragraph.status === 'RUNNING' || $scope.paragraph.status === 'PENDING')) {
|
||||
$scope.runParagraph(editorValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.moveUp = function() {
|
||||
$scope.$emit('moveParagraphUp', $scope.paragraph.id);
|
||||
};
|
||||
|
|
@ -491,7 +504,9 @@ angular.module('zeppelinWebApp')
|
|||
$scope.editor.setHighlightGutterLine(false);
|
||||
$scope.editor.getSession().setUseWrapMode(true);
|
||||
$scope.editor.setTheme('ace/theme/chrome');
|
||||
$scope.editor.focus();
|
||||
if ($scope.paragraphFocused) {
|
||||
$scope.editor.focus();
|
||||
}
|
||||
|
||||
autoAdjustEditorHeight(_editor.container.id);
|
||||
angular.element(window).resize(function() {
|
||||
|
|
@ -591,19 +606,6 @@ angular.module('zeppelinWebApp')
|
|||
|
||||
$scope.setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue());
|
||||
|
||||
$scope.editor.commands.addCommand({
|
||||
name: 'run',
|
||||
bindKey: {win: 'Shift-Enter', mac: 'Shift-Enter'},
|
||||
exec: function(editor) {
|
||||
var editorValue = editor.getValue();
|
||||
if (editorValue) {
|
||||
if (!($scope.paragraph.status === 'RUNNING' || $scope.paragraph.status === 'PENDING')) {
|
||||
$scope.runParagraph(editorValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
readOnly: false
|
||||
});
|
||||
|
||||
// autocomplete on '.'
|
||||
/*
|
||||
|
|
@ -617,6 +619,10 @@ angular.module('zeppelinWebApp')
|
|||
});
|
||||
*/
|
||||
|
||||
// remove binding
|
||||
$scope.editor.commands.bindKey('ctrl-alt-n.', null);
|
||||
|
||||
|
||||
// autocomplete on 'ctrl+.'
|
||||
$scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete');
|
||||
$scope.editor.commands.bindKey('ctrl-space', null);
|
||||
|
|
@ -636,7 +642,7 @@ angular.module('zeppelinWebApp')
|
|||
var numRows;
|
||||
var currentRow;
|
||||
|
||||
if (keyCode === 38 || (keyCode === 80 && e.ctrlKey)) { // UP
|
||||
if (keyCode === 38 || (keyCode === 80 && e.ctrlKey && !e.altKey)) { // UP
|
||||
numRows = $scope.editor.getSession().getLength();
|
||||
currentRow = $scope.editor.getCursorPosition().row;
|
||||
if (currentRow === 0) {
|
||||
|
|
@ -645,7 +651,7 @@ angular.module('zeppelinWebApp')
|
|||
} else {
|
||||
$scope.scrollToCursor($scope.paragraph.id, -1);
|
||||
}
|
||||
} else if (keyCode === 40 || (keyCode === 78 && e.ctrlKey)) { // DOWN
|
||||
} else if (keyCode === 40 || (keyCode === 78 && e.ctrlKey && !e.altKey)) { // DOWN
|
||||
numRows = $scope.editor.getSession().getLength();
|
||||
currentRow = $scope.editor.getCursorPosition().row;
|
||||
if (currentRow === numRows-1) {
|
||||
|
|
@ -766,21 +772,94 @@ angular.module('zeppelinWebApp')
|
|||
}
|
||||
});
|
||||
|
||||
$scope.$on('focusParagraph', function(event, paragraphId, cursorPos) {
|
||||
$scope.$on('keyEvent', function(event, keyEvent) {
|
||||
if ($scope.paragraphFocused) {
|
||||
|
||||
var paragraphId = $scope.paragraph.id;
|
||||
var keyCode = keyEvent.keyCode;
|
||||
var noShortcutDefined = false;
|
||||
var editorHide = $scope.paragraph.config.editorHide;
|
||||
|
||||
if (editorHide && (keyCode === 38 || (keyCode === 80 && keyEvent.ctrlKey && !keyEvent.altKey))) { // up
|
||||
// move focus to previous paragraph
|
||||
$scope.$emit('moveFocusToPreviousParagraph', paragraphId);
|
||||
} else if (editorHide && (keyCode === 40 || (keyCode === 78 && keyEvent.ctrlKey && !keyEvent.altKey))) { // down
|
||||
// move focus to next paragraph
|
||||
$scope.$emit('moveFocusToNextParagraph', paragraphId);
|
||||
} else if (keyEvent.shiftKey && keyCode === 13) { // Shift + Enter
|
||||
$scope.run();
|
||||
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 67) { // Ctrl + Alt + c
|
||||
$scope.cancelParagraph();
|
||||
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 68) { // Ctrl + Alt + d
|
||||
$scope.removeParagraph();
|
||||
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 75) { // Ctrl + Alt + k
|
||||
$scope.moveUp();
|
||||
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 74) { // Ctrl + Alt + j
|
||||
$scope.moveDown();
|
||||
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 66) { // Ctrl + Alt + b
|
||||
$scope.insertNew();
|
||||
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 79) { // Ctrl + Alt + o
|
||||
$scope.toggleOutput();
|
||||
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 69) { // Ctrl + Alt + e
|
||||
$scope.toggleEditor();
|
||||
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 77) { // Ctrl + Alt + m
|
||||
if ($scope.paragraph.config.lineNumbers) {
|
||||
$scope.hideLineNumbers();
|
||||
} else {
|
||||
$scope.showLineNumbers();
|
||||
}
|
||||
} else if (keyEvent.ctrlKey && keyEvent.altKey && ((keyCode >= 48 && keyCode <=57) || keyCode === 189 || keyCode === 187)) { // Ctrl + Alt + [1~9,0,-,=]
|
||||
var colWidth = 12;
|
||||
if (keyCode === 48) {
|
||||
colWidth = 10;
|
||||
} else if (keyCode === 189) {
|
||||
colWidth = 11;
|
||||
} else if (keyCode === 187) {
|
||||
colWidth = 12;
|
||||
} else {
|
||||
colWidth = keyCode - 48;
|
||||
}
|
||||
$scope.paragraph.config.colWidth = colWidth;
|
||||
$scope.changeColWidth();
|
||||
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 84) { // Ctrl + Alt + t
|
||||
if ($scope.paragraph.config.title) {
|
||||
$scope.hideTitle();
|
||||
} else {
|
||||
$scope.showTitle();
|
||||
}
|
||||
} else {
|
||||
noShortcutDefined = true;
|
||||
}
|
||||
|
||||
if (!noShortcutDefined) {
|
||||
keyEvent.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('focusParagraph', function(event, paragraphId, cursorPos, mouseEvent) {
|
||||
if ($scope.paragraph.id === paragraphId) {
|
||||
// focus editor
|
||||
$scope.editor.focus();
|
||||
if (!$scope.paragraph.config.editorHide) {
|
||||
$scope.editor.focus();
|
||||
|
||||
// move cursor to the first row (or the last row)
|
||||
var row;
|
||||
if (cursorPos >= 0) {
|
||||
row = cursorPos;
|
||||
$scope.editor.gotoLine(row, 0);
|
||||
} else {
|
||||
row = $scope.editor.session.getLength();
|
||||
$scope.editor.gotoLine(row, 0);
|
||||
if (!mouseEvent) {
|
||||
// move cursor to the first row (or the last row)
|
||||
var row;
|
||||
if (cursorPos >= 0) {
|
||||
row = cursorPos;
|
||||
$scope.editor.gotoLine(row, 0);
|
||||
} else {
|
||||
row = $scope.editor.session.getLength();
|
||||
$scope.editor.gotoLine(row, 0);
|
||||
}
|
||||
$scope.scrollToCursor($scope.paragraph.id, 0);
|
||||
}
|
||||
}
|
||||
$scope.scrollToCursor($scope.paragraph.id, 0);
|
||||
$scope.handleFocus(true);
|
||||
} else {
|
||||
$scope.editor.blur();
|
||||
$scope.handleFocus(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,18 @@ limitations under the License.
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Run the note
|
||||
Run paragraph
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="keys">
|
||||
<kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">Alt</kbd> + <kbd class="kbd-dark">c</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -56,6 +67,104 @@ limitations under the License.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="keys">
|
||||
<kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">Alt</kbd> + <kbd class="kbd-dark">d</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Remove paragraph
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="keys">
|
||||
<kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">Alt</kbd> + <kbd class="kbd-dark">b</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Insert new paragraph below
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="keys">
|
||||
<kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">Alt</kbd> + <kbd class="kbd-dark">k</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Move paragraph Up
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="keys">
|
||||
<kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">Alt</kbd> + <kbd class="kbd-dark">j</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Move paragraph Down
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="keys">
|
||||
<kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">Alt</kbd> + <kbd class="kbd-dark">o</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Toggle output
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="keys">
|
||||
<kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">Alt</kbd> + <kbd class="kbd-dark">e</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Toggle editor
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="keys">
|
||||
<kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">Alt</kbd> + <kbd class="kbd-dark">m</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Toggle line number
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="keys">
|
||||
<kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">Alt</kbd> + <kbd class="kbd-dark">t</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Toggle title
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="keys">
|
||||
<kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">Alt</kbd> + <kbd class="kbd-dark">1</kbd>~<kbd class="kbd-dark">0</kbd>,<kbd class="kbd-dark">-</kbd>,<kbd class="kbd-dark">+</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
Set paragraph width from 1 to 12
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Control in Note Editor</h4>
|
||||
|
||||
|
|
|
|||