Support of aggregation results

This commit is contained in:
Bruno Bonnin 2015-12-28 15:36:53 +01:00
parent 3bfd97e236
commit 6f76ffd323
11 changed files with 423 additions and 97 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View file

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View file

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View file

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View file

@ -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:
![Elasticsearch - Search with query string](../assets/themes/zeppelin/img/docs-img/elasticsearch-query-string.png)
* With a query containing a multi-value metric aggregation:
![Elasticsearch - Search with aggregation (multi-value metric)](../assets/themes/zeppelin/img/docs-img/elasticsearch-agg-multi-value-metric.png)
* With a query containing a multi-bucket aggregation:
![Elasticsearch - Search with aggregation (multi-bucket)](../assets/themes/zeppelin/img/docs-img/elasticsearch-agg-multi-bucket-pie.png)
#### count
With the `count` command, you can count documents available in some indices and types. You can also provide a query.

View file

@ -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()));
}
}

View file

@ -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() {

View file

@ -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) {

View file

@ -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);
}
});

View file

@ -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>