Merge branch 'master' into ZEPPELIN-1306

This commit is contained in:
astroshim 2016-08-26 10:22:07 +09:00
commit 322d427f26
35 changed files with 865 additions and 177 deletions

View file

@ -1,4 +1,4 @@
#Zeppelin
# Apache Zeppelin
**Documentation:** [User Guide](http://zeppelin.apache.org/docs/latest/index.html)<br/>
**Mailing Lists:** [User and Dev mailing list](http://zeppelin.apache.org/community.html)<br/>

94
docs/CONTRIBUTING.md Normal file
View file

@ -0,0 +1,94 @@
# Contributing to Apache Zeppelin Documentation
## Folder Structure
`docs/` folder is organized as below:
```
docs/
├── _includes/themes/zeppelin
│ ├── _navigation.html
│ └── default.html
├── _layouts
├── _plugins
├── assets/themes/zeppelin -> {ASSET_PATH}
│ ├── bootstrap
│ ├── css
│ ├── img
│ └── js
├── development/ *.md
├── displaysystem/ *.md
├── install/ *.md
├── interpreter/ *.md
├── manual/ *.md
├── quickstart/ *.md
├── rest-api/ *.md
├── security/ *.md
├── storage/ *.md
├── Gemfile
├── Gemfile.lock
├── _config.yml
├── index.md
└── ...
```
- `_navigation.html`: the dropdown menu in navbar
- `default.html` & `_layouts/`: define default HTML layout
- `_plugins/`: custom plugin `*.rb` files can be placed in this folder. See [jekyll/plugins](https://jekyllrb.com/docs/plugins/) for the further information.
- `{ASSET_PATH}/css/style.css`: extra css components can be defined
- `{ASSET_PATH}/img/docs-img/`: image files used for document pages can be placed in this folder
- `{ASSET_PATH}/js/`: extra `.js` files can be placed
- `Gemfile`: defines bundle dependencies. They will be installed by `bundle install`.
- `Gemfile.lock`: when you run `bundle install`, bundler will persist all gems name and their version to this file. For the more details, see [Bundle "The Gemfile Lock"](http://bundler.io/v1.10/man/bundle-install.1.html#THE-GEMFILE-LOCK)
- `documentation_group`: `development/`, `displaysystem/`, `install/`, `interpreter/`...
- `_config.yml`: defines configuration options for docs website. See [jekyll/configuration](https://jekyllrb.com/docs/configuration/) for the other available config variables.
- `index.md`: the main page of `http://zeppelin.apache.org/docs/<ZEPPELIN_VERSION>/`
## Markdown
Zeppelin documentation pages are written with [Markdown](http://daringfireball.net/projects/markdown/). It is possible to use [GitHub flavored syntax](https://help.github.com/categories/writing-on-github/) and intermix plain HTML.
## Front matter
Every page contains [YAML front matter](https://jekyllrb.com/docs/frontmatter/) block in their header. Don't forget to wrap the front matter list with triple-dashed lines(`---`) like below.
The document page should start this triple-dashed lines. Or you will face 404 error, since Jekyll can't find the page.
```
---
layout: page
title: "Apache Zeppelin Tutorial"
description: "This tutorial page contains a short walk-through tutorial that uses Apache Spark backend. Please note that this tutorial is valid for Spark 1.3 and higher."
group: quickstart
---
```
- `layout`: the default layout is `page` which is defined in `_layout/page.html`.
- `title`: the title for the document. Please note that if it needs to include `Zeppelin`, it should be `Apache Zeppelin`, not `Zeppelin`.
- `description`: a short description for the document. One or two sentences would be enough. This description also will be shown as an extract sentence when people search pages.
- `group`: a category of the document page
## Headings
All documents are structured with headings. From these headings, you can automatically generate a **Table of Contents**. There is a simple rule for Zeppelin docs headings.
```
# Level-1 heading <- used only for the main title
## Level-2 heading <- start with this
### Level-3 heading
#### Level-4 heading <- won't be converted in TOC from this level
```
## Table of contents(TOC)
```
<div id="toc"></div>
```
Add this line below `# main title` in order to generate a **Table of Contents**. Headings until `### (Level-3 heading)` are included to TOC.
Default setting options for TOC are definded in [here](https://github.com/apache/zeppelin/blob/master/docs/assets/themes/zeppelin/js/toc.js#L4).
## Adding new pages
If you're going to create new pages, there are some spots you need to add the location of the page.
- **Dropdown menu in navbar**: add your docs location to [_navigation.html](https://github.com/apache/zeppelin/blob/master/docs/_includes/themes/zeppelin/_navigation.html)
- **Main index**: add your docs below [What is the next?](http://zeppelin.apache.org/docs/latest/#what-is-the-next) section in [index.md](https://github.com/apache/zeppelin/blob/master/docs/index.md) with a short description. No need to do this if the page is for **Interpreters**.

View file

@ -1,41 +1,55 @@
## Apache Zeppelin documentation
This readme will walk you through building the Zeppelin documentation, which is included here with the Zeppelin source code.
# Apache Zeppelin documentation
This README will walk you through building the documentation of Apache Zeppelin. The documentation is included here with Apache Zeppelin source code. The online documentation at [https://zeppelin.apache.org/docs/<ZEPPELIN_VERSION>](https://zeppelin.apache.org/docs/latest) is also generated from the files found in here.
## Build documentation
See https://help.github.com/articles/using-jekyll-with-pages#installing-jekyll
Zeppelin is using [Jekyll](https://jekyllrb.com/) which is a static site generator and [Github Pages](https://pages.github.com/) as a site publisher. For the more details, see [help.github.com/articles/about-github-pages-and-jekyll/](https://help.github.com/articles/about-github-pages-and-jekyll/).
**Requirements**
```
ruby --version >= 2.0.0
gem install bundler
# go to /docs under your Zeppelin source
bundle install
# ruby --version >= 2.0.0
# Install Bundler using gem
gem install bundler
cd $ZEPPELIN_HOME/docs
# Install all dependencies declared in the Gemfile
bundle install
```
For the further information about requirements, please see [here](https://help.github.com/articles/setting-up-your-github-pages-site-locally-with-jekyll/#requirements).
For the further information about requirements, please see [here](https://help.github.com/articles/setting-up-your-github-pages-site-locally-with-jekyll/#requirements).
*On OS X 10.9 you may need to do "xcode-select --install"*
On OS X 10.9, you may need to do
```
xcode-select --install
```
## Run website locally
If you don't want to encounter uglily rendered pages, run the documentation site in your local first.
In `$ZEPPELIN_HOME/docs`,
```
bundle exec jekyll serve --watch
```
Using the above command, Jekyll will start a web server at `http://localhost:4000` and watch the `/docs` directory to update.
## Run website
bundle exec jekyll serve --watch
## Contribute to Zeppelin documentation
If you wish to help us and contribute to Zeppelin Documentation, please look at [Zeppelin Documentation's contribution guideline](https://github.com/apache/zeppelin/blob/master/docs/CONTRIBUTING.md).
## Adding a new page
rake page name="new-page.md"
## Bumping up version in a new release
## For committers only
### Bumping up version in a new release
* `ZEPPELIN_VERSION` and `BASE_PATH` property in _config.yml
## Deploy to ASF svnpubsub infra (for committers only)
### Deploy to ASF svnpubsub infra
1. generate static website in `./_site`
```
# go to /docs under Zeppelin source
bundle exec jekyll build --safe

View file

@ -198,7 +198,10 @@ The role of registered interpreters, settings and interpreters group are describ
</tr>
<tr>
<td>Fail code</td>
<td> 500 </td>
<td>
400 if the input json is empty <br/>
500 for any other errors
</td>
</tr>
<tr>
<td>Sample JSON input</td>
@ -219,7 +222,9 @@ The role of registered interpreters, settings and interpreters group are describ
"dependencies": [
{
"groupArtifactVersion": "groupId:artifactId:version",
"exclusions": "groupId:artifactId"
"exclusions": [
"groupId:artifactId"
]
}
]
}
@ -249,7 +254,9 @@ The role of registered interpreters, settings and interpreters group are describ
"dependencies": [
{
"groupArtifactVersion": "groupId:artifactId:version",
"exclusions": "groupId:artifactId"
"exclusions": [
"groupId:artifactId"
]
}
]
}
@ -298,7 +305,9 @@ The role of registered interpreters, settings and interpreters group are describ
"dependencies": [
{
"groupArtifactVersion": "groupId:artifactId:version",
"exclusions": "groupId:artifactId"
"exclusions": [
"groupId:artifactId"
]
}
]
}
@ -328,7 +337,9 @@ The role of registered interpreters, settings and interpreters group are describ
"dependencies": [
{
"groupArtifactVersion": "groupId:artifactId:version",
"exclusions": "groupId:artifactId"
"exclusions": [
"groupId:artifactId"
]
}
]
}

View file

@ -392,6 +392,43 @@ If you work with Apache Zeppelin and find a need for an additional REST API, ple
</tr>
</table>
<br/>
### Get the status of a single paragraph
<table class="table-configuration">
<col width="200">
<tr>
<td>Description</td>
<td>This ```GET``` method gets the status of a single paragraph by the given notebook and paragraph id.
The body field of the returned JSON contains of the array that compose of the paragraph id, paragraph status, paragraph finish date, paragraph started date.
</td>
</tr>
<tr>
<td>URL</td>
<td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/job/[notebookId]/[paragraphId]```</td>
</tr>
<tr>
<td>Success code</td>
<td>200</td>
</tr>
<tr>
<td> Fail code</td>
<td> 500 </td>
</tr>
<tr>
<td> sample JSON response </td>
<td><pre>
{
"status": "OK",
"body": {
"id":"20151121-212654\_766735423",
"status":"FINISHED",
"finished":"Tue Nov 24 14:21:40 KST 2015",
"started":"Tue Nov 24 14:21:39 KST 2015"
}
}</pre></td>
</tr>
</table>
<br/>
### Run a paragraph
<table class="table-configuration">

View file

@ -54,23 +54,15 @@ import java.util.Properties;
* zeppelin.hbase.test.mode: (Testing only) Disable checks for unit and manual tests. Default: false
*/
public class HbaseInterpreter extends Interpreter {
public static final String HBASE_HOME = "hbase.home";
public static final String HBASE_RUBY_SRC = "hbase.ruby.sources";
public static final String HBASE_TEST_MODE = "zeppelin.hbase.test.mode";
private Logger logger = LoggerFactory.getLogger(HbaseInterpreter.class);
private ScriptingContainer scriptingContainer;
private StringWriter writer;
static {
Interpreter.register("hbase", "hbase", HbaseInterpreter.class.getName(),
new InterpreterPropertyBuilder()
.add("hbase.home",
getSystemDefault("HBASE_HOME", "hbase.home", "/usr/lib/hbase/"),
"Installation directory of HBase")
.add("hbase.ruby.sources", "lib/ruby",
"Path to Ruby scripts relative to 'hbase.home'")
.add("zeppelin.hbase.test.mode", "false", "Disable checks for unit and manual tests")
.build());
}
public HbaseInterpreter(Properties property) {
super(property);
}
@ -81,9 +73,9 @@ public class HbaseInterpreter extends Interpreter {
this.writer = new StringWriter();
scriptingContainer.setOutput(this.writer);
if (!Boolean.parseBoolean(getProperty("zeppelin.hbase.test.mode"))) {
String hbase_home = getProperty("hbase.home");
String ruby_src = getProperty("hbase.ruby.sources");
if (!Boolean.parseBoolean(getProperty(HBASE_TEST_MODE))) {
String hbase_home = getProperty(HBASE_HOME);
String ruby_src = getProperty(HBASE_RUBY_SRC);
Path abs_ruby_src = Paths.get(hbase_home, ruby_src).toAbsolutePath();
logger.info("Home:" + hbase_home);
@ -98,7 +90,7 @@ public class HbaseInterpreter extends Interpreter {
logger.info("Absolute Ruby Source:" + abs_ruby_src.toString());
// hirb.rb:41 requires the following system property to be set.
Properties sysProps = System.getProperties();
sysProps.setProperty("hbase.ruby.sources", abs_ruby_src.toString());
sysProps.setProperty(HBASE_RUBY_SRC, abs_ruby_src.toString());
Path abs_hirb_path = Paths.get(hbase_home, "bin/hirb.rb");
try {

View file

@ -0,0 +1,25 @@
[
{
"group": "hbase",
"name": "hbase",
"className": "org.apache.zeppelin.hbase.HbaseInterpreter",
"properties": {
"hbase.home": {
"envName": "HBASE_HOME",
"propertyName": "hbase.home",
"defaultValue": "/usr/lib/hbase/",
"description": "Installation directory of HBase"
},
"hbase.ruby.sources": {
"propertyName": "hbase.ruby.sources",
"defaultValue": "lib/ruby",
"description": "Path to Ruby scripts relative to 'hbase.home'"
},
"zeppelin.hbase.test.mode": {
"propertyName": "zeppelin.hbase.test.mode",
"defaultValue": "false",
"description": "Disable checks for unit and manual tests"
}
}
}
]

View file

@ -486,6 +486,7 @@
<exclude>docs/sitemap.txt</exclude>
<exclude>docs/search_data.json</exclude>
<exclude>**/dependency-reduced-pom.xml</exclude>
<exclude>docs/CONTRIBUTING.md</exclude>
<!-- bundled from anchor -->
<exclude>docs/assets/themes/zeppelin/js/anchor.min.js</exclude>

View file

@ -20,11 +20,11 @@
import sys
import signal
import base64
from io import BytesIO
try:
import StringIO as io
from StringIO import StringIO
except ImportError:
import io as io
from io import StringIO
def intHandler(signum, frame): # Set the signal handler
print ("Paragraph interrupted")
@ -117,6 +117,7 @@ class PyZeppelinContext(object):
def __init__(self):
self.max_result = 1000
self.py3 = bool(sys.version_info >= (3,))
def input(self, name, defaultValue=""):
print(self.errorMsg)
@ -141,14 +142,14 @@ class PyZeppelinContext(object):
"""Pretty prints DF using Table Display System
"""
limit = len(df) > self.max_result
header_buf = io.StringIO("")
header_buf = StringIO("")
header_buf.write(str(df.columns[0]))
for col in df.columns[1:]:
header_buf.write("\t")
header_buf.write(str(col))
header_buf.write("\n")
body_buf = io.StringIO("")
body_buf = StringIO("")
rows = df.head(self.max_result).values if limit else df.values
for row in rows:
body_buf.write(str(row[0]))
@ -168,13 +169,18 @@ class PyZeppelinContext(object):
fmt='png', **kwargs):
"""Matplotlib show function
"""
img = io.StringIO()
if fmt == 'png':
img = BytesIO()
p.savefig(img, format=fmt)
html = "%html <img src={img} width={width}, height={height}>"
img_str = "data:image/png;base64,"
img_str = b"data:image/png;base64,"
img_str += base64.b64encode(img.getvalue().strip())
# Need to do this for python3 compatibility
if self.py3:
img_str = img_str.decode('ascii')
elif fmt == 'svg':
img = StringIO()
p.savefig(img, format=fmt)
html = "%html <div style='width:{width};height:{height}'>{img}<div>"
img_str = img.getvalue()

View file

@ -75,6 +75,7 @@ public class PySparkInterpreter extends Interpreter implements ExecuteResultHand
private ByteArrayOutputStream input;
private String scriptPath;
boolean pythonscriptRunning = false;
private static final int MAX_TIMEOUT_SEC = 10;
public PySparkInterpreter(Properties property) {
super(property);
@ -316,7 +317,7 @@ public class PySparkInterpreter extends Interpreter implements ExecuteResultHand
long startTime = System.currentTimeMillis();
while (pythonScriptInitialized == false
&& pythonscriptRunning
&& System.currentTimeMillis() - startTime < 10 * 1000) {
&& System.currentTimeMillis() - startTime < MAX_TIMEOUT_SEC * 1000) {
try {
pythonScriptInitializeNotifier.wait(1000);
} catch (InterruptedException e) {
@ -423,8 +424,15 @@ public class PySparkInterpreter extends Interpreter implements ExecuteResultHand
}
synchronized (statementFinishedNotifier) {
while (statementOutput == null) {
long startTime = System.currentTimeMillis();
while (statementOutput == null
&& pythonScriptInitialized == false
&& pythonscriptRunning) {
try {
if (System.currentTimeMillis() - startTime < MAX_TIMEOUT_SEC * 1000) {
logger.error("pyspark completion didn't have response for {}sec.", MAX_TIMEOUT_SEC);
break;
}
statementFinishedNotifier.wait(1000);
} catch (InterruptedException e) {
// not working

View file

@ -89,6 +89,8 @@ class Utils {
return true;
} catch (ClassNotFoundException e) {
return false;
} catch (IncompatibleClassChangeError e) {
return false;
}
}

View file

@ -114,6 +114,7 @@ The following components are provided under Apache License.
(Apache 2.0) Utility classes for Jetty (org.mortbay.jetty:jetty-util:6.1.26 - http://javadox.com/org.mortbay.jetty/jetty/6.1.26/overview-tree.html)
(Apache 2.0) Servlet API (org.mortbay.jetty:servlet-api:2.5-20081211 - https://en.wikipedia.org/wiki/Jetty_(web_server))
(Apache 2.0) Google HTTP Client Library for Java (com.google.http-client:google-http-client-jackson2:1.21.0 - https://github.com/google/google-http-java-client/tree/dev/google-http-client-jackson2)
(Apache 2.0) angular-esri-map (https://github.com/Esri/angular-esri-map)
========================================================================
MIT licenses

View file

@ -91,6 +91,9 @@ public class InterpreterRestApi {
try {
NewInterpreterSettingRequest request =
gson.fromJson(message, NewInterpreterSettingRequest.class);
if (request == null) {
return new JsonResponse<>(Status.BAD_REQUEST).build();
}
Properties p = new Properties();
p.putAll(request.getProperties());
InterpreterSetting interpreterSetting = interpreterFactory

View file

@ -116,8 +116,8 @@ public class NotebookRestApi {
* TODO(jl): Fixed the type of HashSet
* https://issues.apache.org/jira/browse/ZEPPELIN-1162
*/
HashMap<String, HashSet> permMap =
gson.fromJson(req, new TypeToken<HashMap<String, HashSet>>() {
HashMap<String, HashSet<String>> permMap =
gson.fromJson(req, new TypeToken<HashMap<String, HashSet<String>>>() {
}.getType());
Note note = notebook.getNote(noteId);
String principal = SecurityUtils.getPrincipal();
@ -133,9 +133,9 @@ public class NotebookRestApi {
ownerPermissionError(userAndRoles, notebookAuthorization.getOwners(noteId))).build();
}
HashSet readers = permMap.get("readers");
HashSet owners = permMap.get("owners");
HashSet writers = permMap.get("writers");
HashSet<String> readers = permMap.get("readers");
HashSet<String> owners = permMap.get("owners");
HashSet<String> writers = permMap.get("writers");
// Set readers, if writers and owners is empty -> set to user requesting the change
if (readers != null && !readers.isEmpty()) {
if (writers.isEmpty()) {
@ -528,6 +528,35 @@ public class NotebookRestApi {
return new JsonResponse<>(Status.OK, null, note.generateParagraphsInfo()).build();
}
/**
* Get notebook paragraph job status REST API
*
* @param notebookId ID of Notebook
* @param paragraphId ID of Paragraph
* @return JSON with status.OK
* @throws IOException, IllegalArgumentException
*/
@GET
@Path("job/{notebookId}/{paragraphId}")
@ZeppelinApi
public Response getNoteParagraphJobStatus(@PathParam("notebookId") String notebookId,
@PathParam("paragraphId") String paragraphId)
throws IOException, IllegalArgumentException {
LOG.info("get notebook paragraph job status.");
Note note = notebook.getNote(notebookId);
if (note == null) {
return new JsonResponse<>(Status.NOT_FOUND, "note not found.").build();
}
Paragraph paragraph = note.getParagraph(paragraphId);
if (paragraph == null) {
return new JsonResponse<>(Status.NOT_FOUND, "paragraph not found.").build();
}
return new JsonResponse<>(Status.OK, null, note.generateSingleParagraphInfo(paragraphId)).
build();
}
/**
* Run paragraph job REST API
*

View file

@ -119,6 +119,15 @@ public class InterpreterRestApiTest extends AbstractTestRestApi {
delete.releaseConnection();
}
@Test
public void testSettingsCreateWithEmptyJson() throws IOException {
// Call Create Setting REST API
PostMethod post = httpPost("/interpreter/setting/", "");
LOG.info("testSettingCRUD create response\n" + post.getResponseBodyAsString());
assertThat("test create method:", post, isBadRequest());
post.releaseConnection();
}
@Test
public void testInterpreterAutoBinding() throws IOException {
// create note

View file

@ -122,6 +122,28 @@ public class NotebookRestApiTest extends AbstractTestRestApi {
ZeppelinServer.notebook.removeNote(note2.getId(), null);
}
@Test
public void testGetNoteParagraphJobStatus() throws IOException {
Note note1 = ZeppelinServer.notebook.createNote(null);
note1.addParagraph();
String paragraphId = note1.getLastParagraph().getId();
GetMethod get = httpGet("/notebook/job/" + note1.getId() + "/" + paragraphId);
assertThat(get, isAllowed());
Map<String, Object> resp = gson.fromJson(get.getResponseBodyAsString(), new TypeToken<Map<String, Object>>() {
}.getType());
Map<String, Set<String>> paragraphStatus = (Map<String, Set<String>>) resp.get("body");
// Check id and status have proper value
assertEquals(paragraphStatus.get("id"), paragraphId);
assertEquals(paragraphStatus.get("status"), "READY");
//cleanup
ZeppelinServer.notebook.removeNote(note1.getId(), null);
}
}

View file

@ -32,7 +32,8 @@
"bootstrap3-dialog": "bootstrap-dialog#~1.34.7",
"handsontable": "~0.24.2",
"moment-duration-format": "^1.3.0",
"select2": "^4.0.3"
"select2": "^4.0.3",
"angular-esri-map": "~2.0.0"
},
"devDependencies": {
"angular-mocks": "1.5.0"

View file

@ -33,7 +33,8 @@
'xeditable',
'ngToast',
'focus-if',
'ngResource'
'ngResource',
'esri.map'
])
.filter('breakFilter', function() {
return function(text) {

View file

@ -68,7 +68,6 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro
var initNotebook = function() {
websocketMsgSrv.getNotebook($routeParams.noteId);
websocketMsgSrv.listRevisionHistory($routeParams.noteId);
var currentRoute = $route.current;
if (currentRoute) {
setTimeout(
@ -333,6 +332,7 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro
} else {
$scope.viewOnly = $scope.note.config.looknfeel === 'report' ? true : false;
}
$scope.note.paragraphs[0].focus = true;
$rootScope.$broadcast('setLookAndFeel', $scope.note.config.looknfeel);
};

View file

@ -47,6 +47,11 @@ limitations under the License.
ng-class="{'active': isGraphMode('scatterChart')}"
ng-click="setGraphMode('scatterChart', true)"><i class="cf cf-scatter-chart"></i>
</button>
<button type="button" class="btn btn-default btn-sm"
ng-if="paragraph.result.type == 'TABLE'"
ng-class="{'active': isGraphMode('map')}"
ng-click="setGraphMode('map', true)"><i class="fa fa-map-marker"></i>
</button>
<button type="button"
ng-if="paragraph.result.type != 'TABLE'"

View file

@ -51,4 +51,12 @@ limitations under the License.
id="p{{paragraph.id}}_scatterChart">
<svg></svg>
</div>
<div ng-if="getGraphMode()=='map'" id="p{{paragraph.id}}_map"
ng-switch="paragraph.config.graph.map.isOnline">
<div ng-switch-when="true"></div>
<span class="map-offline-text" ng-switch-default>
<span>Maps require internet connectivity.</span>
</span>
</div>
</div>

View file

@ -28,4 +28,19 @@ limitations under the License.
show line chart with focus
</label>
</div>
<div ng-if="isGraphMode('map')">
<label>Basemap</label>
<span class="dropdown">
<button type="button" class="btn btn-default btn-sm dropdown-toggle" style="min-width: 0px;" data-toggle="dropdown">
<span ng-bind="paragraph.config.graph.map.baseMapType"></span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" style="min-width: 70px;">
<li ng-repeat="opt in baseMapOption">
<a ng-click="setMapBaseMap(opt)">{{opt}}</a>
</li>
</ul>
</span>
<br/>
</div>
</div>

View file

@ -32,7 +32,7 @@ limitations under the License.
</ul>
</div>
<div class="row" ng-if="getGraphMode()!='scatterChart'">
<div class="row" ng-if="getGraphMode()!='scatterChart' && getGraphMode()!='map'">
<div class="col-md-4">
<span class="columns lightBold">
Keys
@ -165,4 +165,52 @@ limitations under the License.
</span>
</div>
</div>
<div class="row" ng-if="getGraphMode()=='map'">
<div class="col-md-4">
<span class="columns lightBold">
Latitude
<ul data-drop="true"
ng-model="paragraph.config.graph.map.lat"
jqyoui-droppable="{onDrop:'onGraphOptionChange()'}"
class="list-unstyled">
<li ng-if="paragraph.config.graph.map.lat">
<div class="btn btn-primary btn-xs">
{{paragraph.config.graph.map.lat.name}} <span class="fa fa-close" ng-click="removeMapOptionLat($index)"></span>
</div>
</li>
</ul>
</span>
</div>
<div class="col-md-4">
<span class="columns lightBold">
Longitude
<ul data-drop="true"
ng-model="paragraph.config.graph.map.lng"
jqyoui-droppable="{onDrop:'onGraphOptionChange()'}"
class="list-unstyled">
<li ng-if="paragraph.config.graph.map.lng">
<div class="btn btn-primary btn-xs">
{{paragraph.config.graph.map.lng.name}} <span class="fa fa-close" ng-click="removeMapOptionLng($index)"></span>
</div>
</li>
</ul>
</span>
</div>
<div class="col-md-4">
<span class="columns lightBold">
Pin contents
<ul data-drop="true"
ng-model="paragraph.config.graph.map.pinCols"
jqyoui-droppable="{multiple:true, onDrop:'onGraphOptionChange()'}"
class="list-unstyled">
<li ng-repeat="col in paragraph.config.graph.map.pinCols">
<div class="btn btn-primary btn-xs">
{{col.name}} <span class="fa fa-close" ng-click="removeMapOptionPinInfo($index)"></span>
</div>
</li>
</ul>
</span>
</div>
</div>
</div>

View file

@ -16,7 +16,7 @@
angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $rootScope, $route, $window,
$routeParams, $location, $timeout, $compile,
$http, websocketMsgSrv, baseUrlSrv, ngToast,
saveAsService) {
saveAsService, esriLoader) {
var ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_';
$scope.parentNote = null;
$scope.paragraph = null;
@ -93,12 +93,12 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
$scope.parentNote = note;
$scope.originalText = angular.copy(newParagraph.text);
$scope.chart = {};
$scope.baseMapOption = ['Streets', 'Satellite', 'Hybrid', 'Topo', 'Gray', 'Oceans', 'Terrain'];
$scope.colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
$scope.paragraphFocused = false;
if (newParagraph.focus) {
$scope.paragraphFocused = true;
}
if (!$scope.paragraph.config) {
$scope.paragraph.config = {};
}
@ -245,6 +245,22 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
config.graph.scatter = {};
}
if (!config.graph.map) {
config.graph.map = {};
}
if (!config.graph.map.baseMapType) {
config.graph.map.baseMapType = $scope.baseMapOption[0];
}
if (!config.graph.map.isOnline) {
config.graph.map.isOnline = true;
}
if (!config.graph.map.pinCols) {
config.graph.map.pinCols = [];
}
if (config.enabled === undefined) {
config.enabled = true;
}
@ -539,9 +555,10 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
$scope.editor.setHighlightGutterLine(false);
$scope.editor.getSession().setUseWrapMode(true);
$scope.editor.setTheme('ace/theme/chrome');
$scope.editor.setReadOnly($scope.isRunning());
if ($scope.paragraphFocused) {
$scope.editor.focus();
$scope.goToLineEnd();
$scope.goToEnd();
}
autoAdjustEditorHeight(_editor.container.id);
@ -822,8 +839,8 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
return false;
};
$scope.goToLineEnd = function() {
$scope.editor.navigateLineEnd();
$scope.goToEnd = function() {
$scope.editor.navigateFileEnd();
};
$scope.getResultType = function(paragraph) {
@ -907,6 +924,8 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
if (!type || type === 'table') {
setTable($scope.paragraph.result, refresh);
} else if (type === 'map') {
setMap($scope.paragraph.result, refresh);
} else {
setD3Chart(type, $scope.paragraph.result, refresh);
}
@ -1034,7 +1053,7 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
d3g = scatterData.d3g;
$scope.chart[type].xAxis.tickFormat(function(d) {return xAxisTickFormat(d, xLabels);});
$scope.chart[type].yAxis.tickFormat(function(d) {return xAxisTickFormat(d, yLabels);});
$scope.chart[type].yAxis.tickFormat(function(d) {return yAxisTickFormat(d, yLabels);});
// configure how the tooltip looks.
$scope.chart[type].tooltipContent(function(key, x, y, graph, data) {
@ -1076,7 +1095,11 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
xLabels = pivotdata.xLabels;
d3g = pivotdata.d3g;
$scope.chart[type].xAxis.tickFormat(function(d) {return xAxisTickFormat(d, xLabels);});
$scope.chart[type].yAxis.tickFormat(function(d) {return yAxisTickFormat(d);});
if (type === 'stackedAreaChart') {
$scope.chart[type].yAxisTickFormat(function(d) {return yAxisTickFormat(d);});
} else {
$scope.chart[type].yAxis.tickFormat(function(d) {return yAxisTickFormat(d, xLabels);});
}
$scope.chart[type].yAxis.axisLabelDistance(50);
if ($scope.chart[type].useInteractiveGuideline) { // lineWithFocusChart hasn't got useInteractiveGuideline
$scope.chart[type].useInteractiveGuideline(true); // for better UX and performance issue. (https://github.com/novus/nvd3/issues/691)
@ -1131,6 +1154,236 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
$timeout(retryRenderer);
};
var setMap = function(data, refresh) {
var createPinMapLayer = function(pins, cb) {
esriLoader.require(['esri/layers/FeatureLayer'], function(FeatureLayer) {
var pinLayer = new FeatureLayer({
id: 'pins',
spatialReference: $scope.map.spatialReference,
geometryType: 'point',
source: pins,
fields: [],
objectIdField: '_ObjectID',
renderer: $scope.map.pinRenderer,
popupTemplate: {
title: '[{_lng}, {_lat}]',
content: [{
type: 'fields',
fieldInfos: []
}]
}
});
// add user-selected pin info fields to popup
var pinInfoCols = $scope.paragraph.config.graph.map.pinCols;
for (var i = 0; i < pinInfoCols.length; ++i) {
pinLayer.popupTemplate.content[0].fieldInfos.push({
fieldName: pinInfoCols[i].name,
visible: true
});
}
cb(pinLayer);
});
};
var getMapPins = function(cb) {
esriLoader.require(['esri/geometry/Point'], function(Point, FeatureLayer) {
var latCol = $scope.paragraph.config.graph.map.lat;
var lngCol = $scope.paragraph.config.graph.map.lng;
var pinInfoCols = $scope.paragraph.config.graph.map.pinCols;
var pins = [];
// construct objects for pins
if (latCol && lngCol && data.rows) {
for (var i = 0; i < data.rows.length; ++i) {
var row = data.rows[i];
var lng = row[lngCol.index];
var lat = row[latCol.index];
var pin = {
geometry: new Point({
longitude: lng,
latitude: lat,
spatialReference: $scope.map.spatialReference
}),
attributes: {
_ObjectID: i,
_lng: lng,
_lat: lat
}
};
// add pin info from user-selected columns
for (var j = 0; j < pinInfoCols.length; ++j) {
var col = pinInfoCols[j];
pin.attributes[col.name] = row[col.index];
}
pins.push(pin);
}
}
cb(pins);
});
};
var updateMapPins = function() {
var pinLayer = $scope.map.map.findLayerById('pins');
$scope.map.popup.close();
if (pinLayer) {
$scope.map.map.remove(pinLayer);
}
// add pins to map as layer
getMapPins(function(pins) {
createPinMapLayer(pins, function(pinLayer) {
$scope.map.map.add(pinLayer);
if (pinLayer.source.length > 0) {
$scope.map.goTo(pinLayer.source);
}
});
});
};
var createMap = function(mapdiv) {
// prevent zooming with the scroll wheel
var disableZoom = function(e) {
var evt = e || window.event;
evt.cancelBubble = true;
evt.returnValue = false;
if (evt.stopPropagation) {
evt.stopPropagation();
}
};
var eName = window.WheelEvent ? 'wheel' : // Modern browsers
window.MouseWheelEvent ? 'mousewheel' : // WebKit and IE
'DOMMouseScroll'; // Old Firefox
mapdiv.addEventListener(eName, disableZoom, true);
esriLoader.require(['esri/views/MapView',
'esri/Map',
'esri/renderers/SimpleRenderer',
'esri/symbols/SimpleMarkerSymbol'],
function(MapView, Map, SimpleRenderer, SimpleMarkerSymbol) {
$scope.map = new MapView({
container: mapdiv,
map: new Map({
basemap: $scope.paragraph.config.graph.map.baseMapType.toLowerCase()
}),
center: [-106.3468, 56.1304], // Canada (lng, lat)
zoom: 2,
pinRenderer: new SimpleRenderer({
symbol: new SimpleMarkerSymbol({
'color': [255, 0, 0, 0.5],
'size': 16.5,
'outline': {
'color': [0, 0, 0, 1],
'width': 1.125,
},
// map pin SVG path
'path': 'M16,3.5c-4.142,0-7.5,3.358-7.5,7.5c0,4.143,7.5,18.121,7.5,' +
'18.121S23.5,15.143,23.5,11C23.5,6.858,20.143,3.5,16,3.5z ' +
'M16,14.584c-1.979,0-3.584-1.604-3.584-3.584S14.021,7.416,' +
'16,7.416S19.584,9.021,19.584,11S17.979,14.584,16,14.584z'
})
})
});
$scope.map.on('click', function() {
// ArcGIS JS API 4.0 does not account for scrolling or position
// changes by default (this is a bug, to be fixed in the upcoming
// version 4.1; see https://geonet.esri.com/thread/177238#comment-609681).
// This results in a misaligned popup.
// Workaround: manually set popup position to match position of selected pin
if ($scope.map.popup.selectedFeature) {
$scope.map.popup.location = $scope.map.popup.selectedFeature.geometry;
}
});
$scope.map.then(updateMapPins);
});
};
var checkMapOnline = function(cb) {
// are we able to get a response from the ArcGIS servers?
var callback = function(res) {
var online = (res.status > 0);
$scope.paragraph.config.graph.map.isOnline = online;
cb(online);
};
$http.head('//services.arcgisonline.com/arcgis/', {
timeout: 5000,
withCredentials: false
}).then(callback, callback);
};
var renderMap = function() {
var mapdiv = angular.element('#p' + $scope.paragraph.id + '_map')
.css('height', $scope.paragraph.config.graph.height)
.children('div').get(0);
// on chart type change, destroy map to force reinitialization.
if ($scope.map && !refresh) {
$scope.map.map.destroy();
$scope.map.pinRenderer = null;
$scope.map = null;
}
var requireMapCSS = function() {
var url = '//js.arcgis.com/4.0/esri/css/main.css';
if (!angular.element('link[href="' + url + '"]').length) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;
angular.element('head').append(link);
}
};
var requireMapJS = function(cb) {
if (!esriLoader.isLoaded()) {
esriLoader.bootstrap({
url: '//js.arcgis.com/4.0'
}).then(cb);
} else {
cb();
}
};
checkMapOnline(function(online) {
// we need an internet connection to use the map
if (online) {
// create map if not exists.
if (!$scope.map) {
requireMapCSS();
requireMapJS(function() {
createMap(mapdiv);
});
} else {
updateMapPins();
}
}
});
};
var retryRenderer = function() {
if (angular.element('#p' + $scope.paragraph.id + '_map div').length) {
try {
renderMap();
} catch (err) {
console.log('Map drawing error %o', err);
}
} else {
$timeout(retryRenderer,10);
}
};
$timeout(retryRenderer);
};
$scope.setMapBaseMap = function(bm) {
$scope.paragraph.config.graph.map.baseMapType = bm;
if ($scope.map) {
$scope.map.map.basemap = bm.toLowerCase();
}
};
$scope.isGraphMode = function(graphName) {
var activeAppId = _.get($scope.paragraph.config, 'helium.activeApp');
if ($scope.getResultType() === 'TABLE' && $scope.getGraphMode() === graphName && !activeAppId) {
@ -1193,6 +1446,24 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeMapOptionLat = function(idx) {
$scope.paragraph.config.graph.map.lat = null;
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeMapOptionLng = function(idx) {
$scope.paragraph.config.graph.map.lng = null;
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
$scope.removeMapOptionPinInfo = function(idx) {
$scope.paragraph.config.graph.map.pinCols.splice(idx, 1);
clearUnknownColsFromGraphOption();
$scope.setGraphMode($scope.paragraph.config.graph.mode, true, false);
};
/* Clear unknown columns from graph option */
var clearUnknownColsFromGraphOption = function() {
var unique = function(list) {
@ -1223,7 +1494,7 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
}
};
var removeUnknownFromScatterSetting = function(fields) {
var removeUnknownFromFields = function(fields) {
for (var f in fields) {
if (fields[f]) {
var found = false;
@ -1235,7 +1506,7 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
break;
}
}
if (!found) {
if (!found && (fields[f] instanceof Object) && !(fields[f] instanceof Array)) {
fields[f] = null;
}
}
@ -1250,7 +1521,11 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
unique($scope.paragraph.config.graph.groups);
removeUnknown($scope.paragraph.config.graph.groups);
removeUnknownFromScatterSetting($scope.paragraph.config.graph.scatter);
removeUnknownFromFields($scope.paragraph.config.graph.scatter);
unique($scope.paragraph.config.graph.map.pinCols);
removeUnknown($scope.paragraph.config.graph.map.pinCols);
removeUnknownFromFields($scope.paragraph.config.graph.map);
};
/* select default key and value if there're none selected */
@ -1271,6 +1546,23 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
$scope.paragraph.config.graph.scatter.xAxis = $scope.paragraph.result.columnNames[0];
}
}
/* try to find columns for the map logitude and latitude */
var findDefaultMapCol = function(settingName, keyword) {
var col;
if (!$scope.paragraph.config.graph.map[settingName]) {
for (var i = 0; i < $scope.paragraph.result.columnNames.length; ++i) {
col = $scope.paragraph.result.columnNames[i];
if (col.name.toUpperCase().indexOf(keyword) !== -1) {
$scope.paragraph.config.graph.map[settingName] = col;
break;
}
}
}
};
findDefaultMapCol('lat', 'LAT');
findDefaultMapCol('lng', 'LONG');
};
var pivot = function(data) {
@ -2228,6 +2520,7 @@ angular.module('zeppelinWebApp').controller('ParagraphCtrl', function($scope, $r
$scope.paragraph.status = data.paragraph.status;
$scope.paragraph.result = data.paragraph.result;
$scope.paragraph.settings = data.paragraph.settings;
$scope.editor.setReadOnly($scope.isRunning());
if (!$scope.asIframe) {
$scope.paragraph.config = data.paragraph.config;

View file

@ -275,6 +275,10 @@ table.dataTable.table-condensed .sorting_desc:after {
background: none !important;
}
.paragraph-disable {
opacity : 0.6!important;
}
.ace_marker-layer .ace_selection {
z-index: 0 !important;
}
@ -327,12 +331,41 @@ table.dataTable.table-condensed .sorting_desc:after {
.tableDisplay div {
}
.tableDisplay img {
.tableDisplay img:not(.esri-bitmap) {
display: block;
max-width: 100%;
height: auto;
}
.esri-display-object > svg {
overflow: visible;
}
.esri-popup > .esri-docked.esri-dock-to-bottom {
padding: 8px;
margin-top: 0px;
}
.esri-popup-main {
max-height: 100%;
}
span.map-offline-text {
display: table;
width: 100%;
height: 100%;
text-align: center;
}
span.map-offline-text > span {
display: table-cell;
vertical-align: middle;
font-size: 18px;
font-weight: 700;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #212121;
}
.tableDisplay .btn-group span {
margin: 10px 0 0 10px;
font-size: 12px;
@ -350,7 +383,8 @@ table.dataTable.table-condensed .sorting_desc:after {
}
.tableDisplay .option .columns {
.tableDisplay .option .columns,
div.esri-view {
height: 100%;
}

View file

@ -38,16 +38,14 @@ limitations under the License.
<div>
<div ng-show="!paragraph.config.editorHide && !viewOnly" style="margin-bottom:3px;">
<div id="{{paragraph.id}}_editor"
style="opacity: 1;"
class="editor"
ui-ace="{
onLoad : aceLoaded,
require : ['ace/ext/language_tools']
}"
ng-model="paragraph.text"
ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING',
'paragraph-text--dirty' : dirtyText !== originalText && dirtyText !== undefined}">
</div>
ng-class="{'paragraph-disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING',
'paragraph-text--dirty' : dirtyText !== originalText && dirtyText !== undefined}"> </div>
</div>
<div ng-include src="'app/notebook/paragraph/paragraph-progressBar.html'"></div>

View file

@ -17,9 +17,11 @@ angular.module('zeppelinWebApp').directive('ngEnter', function() {
return function(scope, element, attrs) {
element.bind('keydown keypress', function(event) {
if (event.which === 13) {
scope.$apply(function() {
scope.$eval(attrs.ngEnter);
});
if (!event.shiftKey) {
scope.$apply(function() {
scope.$eval(attrs.ngEnter);
});
}
event.preventDefault();
}
});

View file

@ -146,6 +146,7 @@ limitations under the License.
<script src="bower_components/handsontable/dist/handsontable.js"></script>
<script src="bower_components/moment-duration-format/lib/moment-duration-format.js"></script>
<script src="bower_components/select2/dist/js/select2.js"></script>
<script src="bower_components/angular-esri-map/dist/angular-esri-map.js"></script>
<!-- endbower -->
<!-- endbuild -->
<!-- build:js({.tmp,src}) scripts/scripts.js -->

View file

@ -65,6 +65,7 @@ module.exports = function(config) {
'bower_components/handsontable/dist/handsontable.js',
'bower_components/moment-duration-format/lib/moment-duration-format.js',
'bower_components/select2/dist/js/select2.js',
'bower_components/angular-esri-map/dist/angular-esri-map.js',
'bower_components/angular-mocks/angular-mocks.js',
// endbower
'src/app/app.js',

View file

@ -39,7 +39,7 @@ describe('Controller: ParagraphCtrl', function() {
'getResultType', 'loadTableData', 'setGraphMode', 'isGraphMode', 'onGraphOptionChange',
'removeGraphOptionKeys', 'removeGraphOptionValues', 'removeGraphOptionGroups', 'setGraphOptionValueAggr',
'removeScatterOptionXaxis', 'removeScatterOptionYaxis', 'removeScatterOptionGroup',
'removeScatterOptionSize'];
'removeScatterOptionSize', 'removeMapOptionLat', 'removeMapOptionLng', 'removeMapOptionPinInfo'];
functions.forEach(function(fn) {
it('check for scope functions to be defined : ' + fn, function() {

View file

@ -97,13 +97,13 @@ public class InterpreterFactory implements InterpreterGroupFactory {
* This is only references with default settings, name and properties
* key: InterpreterSetting.name
*/
private Map<String, InterpreterSetting> interpreterSettingsRef = new HashMap<>();
private final Map<String, InterpreterSetting> interpreterSettingsRef = new HashMap<>();
/**
* This is used by creating and running Interpreters
* key: InterpreterSetting.id <- This is becuase backward compatibility
*/
private Map<String, InterpreterSetting> interpreterSettings = new HashMap<>();
private final Map<String, InterpreterSetting> interpreterSettings = new HashMap<>();
private Map<String, List<String>> interpreterBindings = new HashMap<>();
private List<RemoteRepository> interpreterRepositories;
@ -175,7 +175,7 @@ public class InterpreterFactory implements InterpreterGroupFactory {
registerInterpreterFromResource(cl, interpreterDirString, interpreterJson);
/**
/*
* TODO(jongyoul)
* - Remove these codes below because of legacy code
* - Support ThreadInterpreter
@ -468,8 +468,6 @@ public class InterpreterFactory implements InterpreterGroupFactory {
* Return ordered interpreter setting list.
* The list does not contain more than one setting from the same interpreter class.
* Order by InterpreterClass (order defined by ZEPPELIN_INTERPRETERS), Interpreter setting name
*
* @return
*/
public List<String> getDefaultInterpreterSettingList() {
// this list will contain default interpreter setting list
@ -529,8 +527,7 @@ public class InterpreterFactory implements InterpreterGroupFactory {
}
/**
* @param group InterpreterSetting reference name
* @param properties
* @param group InterpreterSetting reference name
* @return
*/
public InterpreterSetting add(String group, ArrayList<InterpreterInfo> interpreterInfos,
@ -579,8 +576,8 @@ public class InterpreterFactory implements InterpreterGroupFactory {
} else {
interpreterSetting =
new InterpreterSetting(group, null, interpreterInfos, properties, dependencies,
option, path);
new InterpreterSetting(group, null, interpreterInfos, properties, dependencies, option,
path);
interpreterSettingsRef.put(group, interpreterSetting);
}
}
@ -1193,6 +1190,16 @@ public class InterpreterFactory implements InterpreterGroupFactory {
return interpreters.get(0);
}
}
// Support the legacy way to use it
for (InterpreterSetting s : settings) {
if (s.getGroup().equals(replName)) {
List<Interpreter> interpreters = createOrGetInterpreterList(noteId, s);
if (null != interpreters) {
return interpreters.get(0);
}
}
}
}
// dev interpreter

View file

@ -411,24 +411,40 @@ public class Note implements Serializable, ParagraphJobListener {
List<Map<String, String>> paragraphsInfo = new LinkedList<>();
synchronized (paragraphs) {
for (Paragraph p : paragraphs) {
Map<String, String> info = new HashMap<>();
info.put("id", p.getId());
info.put("status", p.getStatus().toString());
if (p.getDateStarted() != null) {
info.put("started", p.getDateStarted().toString());
}
if (p.getDateFinished() != null) {
info.put("finished", p.getDateFinished().toString());
}
if (p.getStatus().isRunning()) {
info.put("progress", String.valueOf(p.progress()));
}
Map<String, String> info = populatePragraphInfo(p);
paragraphsInfo.add(info);
}
}
return paragraphsInfo;
}
public Map<String, String> generateSingleParagraphInfo(String paragraphId) {
synchronized (paragraphs) {
for (Paragraph p : paragraphs) {
if (p.getId().equals(paragraphId)) {
return populatePragraphInfo(p);
}
}
return new HashMap<>();
}
}
private Map<String, String> populatePragraphInfo(Paragraph p) {
Map<String, String> info = new HashMap<>();
info.put("id", p.getId());
info.put("status", p.getStatus().toString());
if (p.getDateStarted() != null) {
info.put("started", p.getDateStarted().toString());
}
if (p.getDateFinished() != null) {
info.put("finished", p.getDateFinished().toString());
}
if (p.getStatus().isRunning()) {
info.put("progress", String.valueOf(p.progress()));
}
return info;
}
/**
* Run all paragraphs sequentially.
*/

View file

@ -33,6 +33,8 @@ import org.apache.zeppelin.user.AuthenticationInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
@ -191,20 +193,45 @@ public class ZeppelinHubRepo implements NotebookRepo {
@Override
public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject)
throws IOException {
// Auto-generated method stub
return null;
if (StringUtils.isBlank(noteId)) {
return null;
}
String endpoint = Joiner.on("/").join(noteId, "checkpoint");
String content = GSON.toJson(ImmutableMap.of("message", checkpointMsg));
String response = restApiClient.asyncPutWithResponseBody(endpoint, content);
return GSON.fromJson(response, Revision.class);
}
@Override
public Note get(String noteId, String revId, AuthenticationInfo subject) throws IOException {
// Auto-generated method stub
return null;
if (StringUtils.isBlank(noteId) || StringUtils.isBlank(revId)) {
return EMPTY_NOTE;
}
String endpoint = Joiner.on("/").join(noteId, "checkpoint", revId);
String response = restApiClient.asyncGet(endpoint);
Note note = GSON.fromJson(response, Note.class);
if (note == null) {
return EMPTY_NOTE;
}
LOG.info("ZeppelinHub REST API get note {} revision {}", noteId, revId);
return note;
}
@Override
public List<Revision> revisionHistory(String noteId, AuthenticationInfo subject) {
// Auto-generated method stub
return null;
if (StringUtils.isBlank(noteId)) {
return Collections.emptyList();
}
String endpoint = Joiner.on("/").join(noteId, "checkpoint");
List<Revision> history = Collections.emptyList();
try {
String response = restApiClient.asyncGet(endpoint);
history = GSON.fromJson(response, new TypeToken<List<Revision>>(){}.getType());
} catch (IOException e) {
LOG.error("Cannot get note history", e);
}
return history;
}
}

View file

@ -25,9 +25,8 @@ import java.util.concurrent.TimeoutException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.InputStreamResponseListener;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
@ -115,89 +114,66 @@ public class ZeppelinhubRestApiHandler {
}
public String asyncGet(String argument) throws IOException {
String note = StringUtils.EMPTY;
InputStreamResponseListener listener = new InputStreamResponseListener();
client.newRequest(zepelinhubUrl + argument)
.header(ZEPPELIN_TOKEN_HEADER, token)
.send(listener);
// Wait for the response headers to arrive
Response response;
try {
response = listener.get(30, TimeUnit.SECONDS);
} catch (InterruptedException | TimeoutException | ExecutionException e) {
LOG.error("Cannot perform Get request to ZeppelinHub", e);
throw new IOException("Cannot load note from ZeppelinHub", e);
}
int code = response.getStatus();
if (code == 200) {
try (InputStream responseContent = listener.getInputStream()) {
note = IOUtils.toString(responseContent, "UTF-8");
}
} else {
LOG.error("ZeppelinHub Get {} returned with status {} ", zepelinhubUrl + argument, code);
throw new IOException("Cannot load note from ZeppelinHub");
}
return note;
return sendToZeppelinHub(HttpMethod.GET, zepelinhubUrl + argument);
}
public String asyncPutWithResponseBody(String url, String json) throws IOException {
if (StringUtils.isBlank(url) || StringUtils.isBlank(json)) {
LOG.error("Empty note, cannot send it to zeppelinHub");
throw new IOException("Cannot send emtpy note to zeppelinHub");
}
return sendToZeppelinHub(HttpMethod.PUT, zepelinhubUrl + url, json);
}
public void asyncPut(String jsonNote) throws IOException {
if (StringUtils.isBlank(jsonNote)) {
LOG.error("Cannot save empty note/string to ZeppelinHub");
return;
}
client.newRequest(zepelinhubUrl).method(HttpMethod.PUT)
.header(ZEPPELIN_TOKEN_HEADER, token)
.content(new StringContentProvider(jsonNote, "UTF-8"), "application/json;charset=UTF-8")
.send(new BufferingResponseListener() {
@Override
public void onComplete(Result res) {
if (!res.isFailed() && res.getResponse().getStatus() == 200) {
LOG.info("Successfully saved note to ZeppelinHub with {}",
res.getResponse().getStatus());
} else {
LOG.warn("Failed to save note to ZeppelinHub with HttpStatus {}",
res.getResponse().getStatus());
}
}
@Override
public void onFailure(Response response, Throwable failure) {
LOG.error("Failed to save note to ZeppelinHub: {}", response.getReason(), failure);
}
});
sendToZeppelinHub(HttpMethod.PUT, zepelinhubUrl, jsonNote);
}
public void asyncDel(String argument) {
public void asyncDel(String argument) throws IOException {
if (StringUtils.isBlank(argument)) {
LOG.error("Cannot delete empty note from ZeppelinHub");
return;
}
client.newRequest(zepelinhubUrl + argument)
.method(HttpMethod.DELETE)
.header(ZEPPELIN_TOKEN_HEADER, token)
.send(new BufferingResponseListener() {
sendToZeppelinHub(HttpMethod.DELETE, zepelinhubUrl + argument);
}
private String sendToZeppelinHub(HttpMethod method, String url) throws IOException {
return sendToZeppelinHub(method, url, StringUtils.EMPTY);
}
private String sendToZeppelinHub(HttpMethod method, String url, String json) throws IOException {
InputStreamResponseListener listener = new InputStreamResponseListener();
Response response;
String data;
@Override
public void onComplete(Result res) {
if (!res.isFailed() && res.getResponse().getStatus() == 200) {
LOG.info("Successfully removed note from ZeppelinHub with {}",
res.getResponse().getStatus());
} else {
LOG.warn("Failed to remove note from ZeppelinHub with HttpStatus {}",
res.getResponse().getStatus());
}
}
Request request = client.newRequest(url).method(method).header(ZEPPELIN_TOKEN_HEADER, token);
if ((method.equals(HttpMethod.PUT) || method.equals(HttpMethod.POST)) &&
!StringUtils.isBlank(json)) {
request.content(new StringContentProvider(json, "UTF-8"), "application/json;charset=UTF-8");
}
request.send(listener);
@Override
public void onFailure(Response response, Throwable failure) {
LOG.error("Failed to remove note from ZeppelinHub: {}", response.getReason(), failure);
}
});
try {
response = listener.get(30, TimeUnit.SECONDS);
} catch (InterruptedException | TimeoutException | ExecutionException e) {
LOG.error("Cannot perform {} request to ZeppelinHub", method, e);
throw new IOException("Cannot perform " + method + " request to ZeppelinHub", e);
}
int code = response.getStatus();
if (code == 200) {
try (InputStream responseContent = listener.getInputStream()) {
data = IOUtils.toString(responseContent, "UTF-8");
}
} else {
LOG.error("ZeppelinHub {} {} returned with status {} ", method, url, code);
throw new IOException("Cannot perform " + method + " request to ZeppelinHub");
}
return data;
}
public void close() {

View file

@ -155,5 +155,6 @@ public class InterpreterFactoryTest {
}});
assertEquals("className1", factory.getInterpreter("note", "test-group1").getClassName());
assertEquals("className1", factory.getInterpreter("note", "group1").getClassName());
}
}