Merge remote-tracking branch 'upstream/master' into ZEPPELIN-2403

# Conflicts:
#	zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSetting.java
#	zeppelin-zengine/src/main/java/org/apache/zeppelin/interpreter/InterpreterSettingManager.java
This commit is contained in:
Tinkoff DWH 2017-06-23 07:26:34 +05:00
commit fd25c467ac
95 changed files with 3414 additions and 483 deletions

3
.gitignore vendored
View file

@ -122,3 +122,6 @@ tramp
# tmp files
/tmp/
# Git properties
**/git.properties

View file

@ -49,10 +49,9 @@ matrix:
#
# Several tests were excluded from this configuration due to the following issues:
# HeliumApplicationFactoryTest - https://issues.apache.org/jira/browse/ZEPPELIN-2470
# ZeppelinRestApiTest - https://issues.apache.org/jira/browse/ZEPPELIN-2473
# After issues are fixed these tests need to be included back by removing them from the "-Dtests.to.exclude" property
- jdk: "oraclejdk7"
env: SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pscalding -Phelium-dev -Pexamples -Pscala-2.11" BUILD_FLAG="package -Pbuild-distr -DskipRat" TEST_FLAG="verify -Pusing-packaged-distr -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_PROJECTS="-Dtests.to.exclude=**/ZeppelinSparkClusterTest.java,**/org.apache.zeppelin.spark.*,**/HeliumApplicationFactoryTest.java,**/ZeppelinRestApiTest.java -DfailIfNoTests=false"
env: SCALA_VER="2.11" SPARK_VER="2.1.0" HADOOP_VER="2.6" PROFILE="-Pweb-ci -Pscalding -Phelium-dev -Pexamples -Pscala-2.11" BUILD_FLAG="package -Pbuild-distr -DskipRat" TEST_FLAG="verify -Pusing-packaged-distr -DskipRat" MODULES="-pl ${INTERPRETERS}" TEST_PROJECTS="-Dtests.to.exclude=**/ZeppelinSparkClusterTest.java,**/org.apache.zeppelin.spark.*,**/HeliumApplicationFactoryTest.java -DfailIfNoTests=false"
# Test selenium with spark module for 1.6.3
- jdk: "oraclejdk7"
@ -60,7 +59,7 @@ matrix:
# Test interpreter modules
- jdk: "oraclejdk7"
env: SCALA_VER="2.10" PROFILE="-Pscalding" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="test -DskipRat" MODULES="-pl $(echo .,zeppelin-interpreter,${INTERPRETERS} | sed 's/!//g')" TEST_PROJECTS=""
env: SCALA_VER="2.10" PROFILE="-Pscalding" BUILD_FLAG="package -DskipTests -DskipRat -Pr" TEST_FLAG="test -DskipRat" MODULES="-pl $(echo .,zeppelin-interpreter,${INTERPRETERS} | sed 's/!//g')" TEST_PROJECTS=""
# Test spark module for 2.1.0 with scala 2.11, livy
- jdk: "oraclejdk7"
@ -94,7 +93,7 @@ before_install:
- changedfiles=$(git diff --name-only $TRAVIS_COMMIT_RANGE 2>/dev/null) || changedfiles=""
- echo $changedfiles
- hasbowerchanged=$(echo $changedfiles | grep -c "bower.json" || true);
- gitlog=$(git log $TRAVIS_COMMIT_RANGE 2>/dev/null) || gitlog=""
- gitlog=$(git log $TRAVIS_COMMIT_RANGE 2>/dev/null) || gitlog=""
- clearcache=$(echo $gitlog | grep -c -E "clear bower|bower clear" || true)
- if [ "$hasbowerchanged" -gt 0 ] || [ "$clearcache" -gt 0 ]; then echo "Clearing bower_components cache"; rm -r zeppelin-web/bower_components; npm cache clear; else echo "Using cached bower_components."; fi
- echo "MAVEN_OPTS='-Xms1024M -Xmx2048M -XX:MaxPermSize=1024m -XX:-UseGCOverheadLimit -Dorg.slf4j.simpleLogger.defaultLogLevel=warn'" >> ~/.mavenrc

View file

@ -18,7 +18,8 @@
[users]
# List of users with their password allowed to access Zeppelin.
# To use a different strategy (LDAP / Database / ...) check the shiro doc at http://shiro.apache.org/configuration.html#Configuration-INISections
admin = password1, admin
# To enable admin user, uncomment the following line and set an appropriate password.
#admin = password1, admin
user1 = password2, role1, role2
user2 = password3, role3
user3 = password4, role2
@ -73,14 +74,22 @@ role3 = *
admin = *
[urls]
# This section is used for url-based security.
# You can secure interpreter, configuration and credential information by urls. Comment or uncomment the below urls that you want to hide.
# This section is used for url-based security. For details see the shiro.ini documentation.
#
# You can secure interpreter, configuration and credential information by urls.
# Comment or uncomment the below urls that you want to hide:
# anon means the access is anonymous.
# authc means Form based Auth Security
# To enfore security, comment the line below and uncomment the next one
# authc means form based auth Security.
#
# IMPORTANT: Order matters: URL path expressions are evaluated against an incoming request
# in the order they are defined and the FIRST MATCH WINS.
#
# To allow anonymous access to all but the stated urls,
# uncomment the line second last line (/** = anon) and comment the last line (/** = authc)
#
/api/version = anon
#/api/interpreter/** = authc, roles[admin]
#/api/configurations/** = authc, roles[admin]
#/api/credential/** = authc, roles[admin]
/api/interpreter/** = authc, roles[admin]
/api/configurations/** = authc, roles[admin]
/api/credential/** = authc, roles[admin]
#/** = anon
/** = authc

View file

@ -39,7 +39,9 @@ REM set ZEPPELIN_IDENT_STRING REM A string representing this instance of zep
REM set ZEPPELIN_NICENESS REM The scheduling priority for daemons. Defaults to 0.
REM set ZEPPELIN_INTERPRETER_LOCALREPO REM Local repository for interpreter's additional dependency loading
REM set ZEPPELIN_INTERPRETER_DEP_MVNREPO REM Maven principal repository for interpreter's additional dependency loading
REM set ZEPPELIN_HELIUM_NPM_REGISTRY REM Remote Npm registry for Helium dependency loader
REM set ZEPPELIN_HELIUM_NODE_INSTALLER_URL REM Remote Node installer url for Helium dependency loader
REM set ZEPPELIN_HELIUM_NPM_INSTALLER_URL REM Remote Npm installer url for Helium dependency loader
REM set ZEPPELIN_HELIUM_YARNPKG_INSTALLER_URL REM Remote Yarn package installer url for Helium dependency loader
REM set ZEPPELIN_NOTEBOOK_STORAGE REM Refers to pluggable notebook storage class, can have two classes simultaneously with a sync between them (e.g. local and remote).
REM set ZEPPELIN_NOTEBOOK_ONE_WAY_SYNC REM If there are multiple notebook storages, should we treat the first one as the only source of truth?

View file

@ -44,7 +44,9 @@
# export ZEPPELIN_NICENESS # The scheduling priority for daemons. Defaults to 0.
# export ZEPPELIN_INTERPRETER_LOCALREPO # Local repository for interpreter's additional dependency loading
# export ZEPPELIN_INTERPRETER_DEP_MVNREPO # Remote principal repository for interpreter's additional dependency loading
# export ZEPPELIN_HELIUM_NPM_REGISTRY # Remote Npm registry for Helium dependency loader
# export ZEPPELIN_HELIUM_NODE_INSTALLER_URL # Remote Node installer url for Helium dependency loader
# export ZEPPELIN_HELIUM_NPM_INSTALLER_URL # Remote Npm installer url for Helium dependency loader
# export ZEPPELIN_HELIUM_YARNPKG_INSTALLER_URL # Remote Yarn package installer url for Helium dependency loader
# export ZEPPELIN_NOTEBOOK_STORAGE # Refers to pluggable notebook storage class, can have two classes simultaneously with a sync between them (e.g. local and remote).
# export ZEPPELIN_NOTEBOOK_ONE_WAY_SYNC # If there are multiple notebook storages, should we treat the first one as the only source of truth?
# export ZEPPELIN_NOTEBOOK_PUBLIC # Make notebook public by default when created, private otherwise

View file

@ -252,9 +252,21 @@
</property>
<property>
<name>zeppelin.helium.npm.registry</name>
<name>zeppelin.helium.node.installer.url</name>
<value>https://nodejs.org/dist/</value>
<description>Remote Node installer url for Helium dependency loader</description>
</property>
<property>
<name>zeppelin.helium.npm.installer.url</name>
<value>http://registry.npmjs.org/</value>
<description>Remote Npm registry for Helium dependency loader</description>
<description>Remote Npm installer url for Helium dependency loader</description>
</property>
<property>
<name>zeppelin.helium.yarnpkg.installer.url</name>
<value>https://github.com/yarnpkg/yarn/releases/download/</value>
<description>Remote Yarn package installer url for Helium dependency loader</description>
</property>
<property>

View file

@ -88,6 +88,7 @@
<li><a href="{{BASE_PATH}}/displaysystem/basicdisplaysystem.html#text">Text</a></li>
<li><a href="{{BASE_PATH}}/displaysystem/basicdisplaysystem.html#html">Html</a></li>
<li><a href="{{BASE_PATH}}/displaysystem/basicdisplaysystem.html#table">Table</a></li>
<li><a href="{{BASE_PATH}}/displaysystem/basicdisplaysystem.html#network">Network</a></li>
<li role="separator" class="divider"></li>
<li class="title"><span><b>Angular API</b><span></li>
<li><a href="{{BASE_PATH}}/displaysystem/back-end-angular.html">Angular (backend API)</a></li>

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -61,3 +61,104 @@ If table contents start with `%html`, it is interpreted as an HTML.
<img src="../assets/themes/zeppelin/img/screenshots/display_table_html.png" />
> **Note :** Display system is backend independent.
## Network
With the `%network` directive, Zeppelin treats your output as a graph. Zeppelin can leverage the Property Graph Model.
### What is the Labelled Property Graph Model?
A [Property Graph](https://github.com/tinkerpop/gremlin/wiki/Defining-a-Property-Graph) is a graph that has these elements:
* a set of vertices
* each vertex has a unique identifier.
* each vertex has a set of outgoing edges.
* each vertex has a set of incoming edges.
* each vertex has a collection of properties defined by a map from key to value
* a set of edges
* each edge has a unique identifier.
* each edge has an outgoing tail vertex.
* each edge has an incoming head vertex.
* each edge has a label that denotes the type of relationship between its two vertices.
* each edge has a collection of properties defined by a map from key to value.
<img src="https://github.com/tinkerpop/gremlin/raw/master/doc/images/graph-example-1.jpg" />
A [Labelled Property Graph](https://neo4j.com/developer/graph-database/#property-graph) is a Property Graph where the nodes can be tagged with **labels** representing their different roles in the graph model
<img src="http://s3.amazonaws.com/dev.assets.neo4j.com/wp-content/uploads/property_graph_model.png" />
### What are the APIs?
The new NETWORK visualization is based on json with the following params:
* "nodes" (mandatory): list of nodes of the graph every node can have the following params:
* "id" (mandatory): the id of the node (must be unique);
* "label": the main Label of the node;
* "labels": the list of the labels of the node;
* "data": the data attached to the node;
* "edges": list of the edges of the graph;
* "id" (mandatory): the id of the edge (must be unique);
* "source" (mandatory): the id of source node of the edge;
* "target" (mandatory): the id of target node of the edge;
* "label": the main type of the edge;
* "data": the data attached to the edge;
* "labels": a map (K, V) where K is the node label and V is the color of the node;
* "directed": (true/false, default false) wich tells if is directed graph or not;
* "types": a *distinct* list of the edge types of the graph
If you click on a node or edge on the bottom of the paragraph you find a list of entity properties
<img src="../assets/themes/zeppelin/img/screenshots/display_network.png" />
This kind of graph can be easily *flatten* in order to support other visualization formats provided by Zeppelin.
<img src="../assets/themes/zeppelin/img/screenshots/display_network_flatten.png" />
### How to use it?
An example of a simple graph
```
%spark
print(s"""
%network {
"nodes": [
{"id": 1},
{"id": 2},
{"id": 3}
],
"edges": [
{"source": 1, "target": 2, "id" : 1},
{"source": 2, "target": 3, "id" : 2},
{"source": 1, "target": 2, "id" : 3},
{"source": 1, "target": 2, "id" : 4},
{"source": 2, "target": 1, "id" : 5},
{"source": 2, "target": 1, "id" : 6}
]
}
""")
```
that will look like:
<img src="../assets/themes/zeppelin/img/screenshots/display_simple_network.png" />
A little more complex graph:
```
%spark
print(s"""
%network {
"nodes": [{"id": 1, "label": "User", "data": {"fullName":"Andrea Santurbano"}},{"id": 2, "label": "User", "data": {"fullName":"Lee Moon Soo"}},{"id": 3, "label": "Project", "data": {"name":"Zeppelin"}}],
"edges": [{"source": 2, "target": 1, "id" : 1, "label": "HELPS"},{"source": 2, "target": 3, "id" : 2, "label": "CREATE"},{"source": 1, "target": 3, "id" : 3, "label": "CONTRIBUTE_TO", "data": {"oldPR": "https://github.com/apache/zeppelin/pull/1582"}}],
"labels": {"User": "#8BC34A", "Project": "#3071A9"},
"directed": true,
"types": ["HELPS", "CREATE", "CONTRIBUTE_TO"]
}
""")
```
that will look like:
<img src="../assets/themes/zeppelin/img/screenshots/display_complex_network.png" />

View file

@ -282,10 +282,22 @@ If both are defined, then the **environment variables** will take priority.
<td>Local repository for dependency loader.<br>ex)visualiztion modules of npm.</td>
</tr>
<tr>
<td><h6 class="properties">ZEPPELIN_HELIUM_NPM_REGISTRY</h6></td>
<td><h6 class="properties">zeppelin.helium.npm.registry</h6></td>
<td><h6 class="properties">ZEPPELIN_HELIUM_NODE_INSTALLER_URL</h6></td>
<td><h6 class="properties">zeppelin.helium.node.installer.url</h6></td>
<td>https://nodejs.org/dist/</td>
<td>Remote Node installer url for Helium dependency loader</td>
</tr>
<tr>
<td><h6 class="properties">ZEPPELIN_HELIUM_NPM_INSTALLER_URL</h6></td>
<td><h6 class="properties">zeppelin.helium.npm.installer.url</h6></td>
<td>http://registry.npmjs.org/</td>
<td>Remote Npm registry for Helium dependency loader</td>
<td>Remote Npm installer url for Helium dependency loader</td>
</tr>
<tr>
<td><h6 class="properties">ZEPPELIN_HELIUM_YARNPKG_INSTALLER_URL</h6></td>
<td><h6 class="properties">zeppelin.helium.yarnpkg.installer.url</h6></td>
<td>https://github.com/yarnpkg/yarn/releases/download/</td>
<td>Remote Yarn package installer url for Helium dependency loader</td>
</tr>
<tr>
<td><h6 class="properties">ZEPPELIN_WEBSOCKET_MAX_TEXT_MESSAGE_SIZE</h6></td>

View file

@ -33,6 +33,45 @@ If you work with Apache Zeppelin and find a need for an additional REST API, ple
## Zeppelin Server REST API list
### Get Zeppelin version
<table class="table-configuration">
<col width="200">
<tr>
<td>Description</td>
<td>This ```GET``` method returns Zeppelin version</td>
</tr>
<tr>
<td>URL</td>
<td>```http://[zeppelin-server]:[zeppelin-port]/api/version```</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",
"message": "Zeppelin version",
"body": [
{
"version": "0.8.0",
"git-commit-id": "abc0123",
"git-timestamp": "2017-01-02 03:04:05"
}
]
}
</pre>
</td>
</tr>
</table>
### Change the log level of Zeppelin Server
<table class="table-configuration">
<col width="200">
@ -49,29 +88,27 @@ If you work with Apache Zeppelin and find a need for an additional REST API, ple
<td>200</td>
</tr>
<tr>
<td> Fail code</td>
<td> 406 </td>
<td>Fail code</td>
<td>406</td>
</tr>
<tr>
<td> sample JSON response
</td>
<td>sample JSON response</td>
<td>
<pre>
{
"status": "OK"
}
}
</pre>
</td>
</tr>
<tr>
<td> sample error JSON response
</td>
<td>sample error JSON response</td>
<td>
<pre>
{
"status":"NOT_ACCEPTABLE",
"message":"Please check LOG level specified. Valid values: DEBUG, ERROR, FATAL, INFO, TRACE, WARN"
}
}
</pre>
</td>
</tr>

View file

@ -341,7 +341,17 @@ public abstract class BaseLivyInterpreter extends Interpreter {
private InterpreterResult getResultFromStatementInfo(StatementInfo stmtInfo,
boolean displayAppInfo) {
if (stmtInfo.output != null && stmtInfo.output.isError()) {
return new InterpreterResult(InterpreterResult.Code.ERROR, stmtInfo.output.evalue);
InterpreterResult result = new InterpreterResult(InterpreterResult.Code.ERROR);
StringBuilder sb = new StringBuilder();
sb.append(stmtInfo.output.evalue);
// in case evalue doesn't have newline char
if (!stmtInfo.output.evalue.contains("\n"))
sb.append("\n");
if (stmtInfo.output.traceback != null) {
sb.append(StringUtils.join(stmtInfo.output.traceback));
}
result.add(sb.toString());
return result;
} else if (stmtInfo.isCancelled()) {
// corner case, output might be null if it is cancelled.
return new InterpreterResult(InterpreterResult.Code.ERROR, "Job is cancelled");
@ -659,7 +669,18 @@ public abstract class BaseLivyInterpreter extends Interpreter {
}
public static StatementInfo fromJson(String json) {
return gson.fromJson(json, StatementInfo.class);
String right_json = "";
try {
gson.fromJson(json, StatementInfo.class);
right_json = json;
} catch (Exception e) {
if (json.contains("\"traceback\":{}")) {
LOGGER.debug("traceback type mismatch, replacing the mismatching part ");
right_json = json.replace("\"traceback\":{}", "\"traceback\":[]");
LOGGER.debug("new json string is {}", right_json);
}
}
return gson.fromJson(right_json, StatementInfo.class);
}
public boolean isAvailable() {
@ -676,7 +697,7 @@ public abstract class BaseLivyInterpreter extends Interpreter {
public Data data;
public String ename;
public String evalue;
public Object traceback;
public String[] traceback;
public TableMagic tableMagic;
public boolean isError() {

View file

@ -523,7 +523,7 @@ public class LivyInterpreterIT {
}
@Test
public void testPySparkInterpreter() {
public void testPySparkInterpreter() throws LivyException {
if (!checkPreCondition()) {
return;
}
@ -536,6 +536,24 @@ public class LivyInterpreterIT {
"title", "text", authInfo, null, null, null, null, null, output);
pysparkInterpreter.open();
// test traceback msg
try {
pysparkInterpreter.getLivyVersion();
// for livy version >=0.3 , input some erroneous spark code, check the shown result is more than one line
InterpreterResult result = pysparkInterpreter.interpret("sc.parallelize(wrongSyntax(1, 2)).count()", context);
assertEquals(InterpreterResult.Code.ERROR, result.code());
assertTrue(result.message().get(0).getData().split("\n").length>1);
assertTrue(result.message().get(0).getData().contains("Traceback"));
} catch (APINotFoundException e) {
// only livy 0.2 can throw this exception since it doesn't have /version endpoint
// in livy 0.2, most error msg is encapsulated in evalue field, only print(a) in pyspark would return none-empty
// traceback
InterpreterResult result = pysparkInterpreter.interpret("print(a)", context);
assertEquals(InterpreterResult.Code.ERROR, result.code());
assertTrue(result.message().get(0).getData().split("\n").length>1);
assertTrue(result.message().get(0).getData().contains("Traceback"));
}
try {
InterpreterResult result = pysparkInterpreter.interpret("sc.version", context);
assertEquals(InterpreterResult.Code.SUCCESS, result.code());
@ -763,6 +781,7 @@ public class LivyInterpreterIT {
}
}
private boolean isSpark2(BaseLivyInterpreter interpreter, InterpreterContext context) {
InterpreterResult result = null;
if (interpreter instanceof LivySparkRInterpreter) {

View file

@ -83,7 +83,7 @@
"msg": [
{
"type": "TEXT",
"data": "\nimport org.apache.commons.io.IOUtils\n\nimport java.net.URL\n\nimport java.nio.charset.Charset\n\nbankText: org.apache.spark.rdd.RDD[String] \u003d ParallelCollectionRDD[0] at parallelize at \u003cconsole\u003e:32\n\ndefined class Bank\n\nbank: org.apache.spark.sql.DataFrame \u003d [age: int, job: string ... 3 more fields]\n\nwarning: there were 1 deprecation warning(s); re-run with -deprecation for details\n"
"data": "import org.apache.commons.io.IOUtils\nimport java.net.URL\nimport java.nio.charset.Charset\nbankText: org.apache.spark.rdd.RDD[String] \u003d ParallelCollectionRDD[36] at parallelize at \u003cconsole\u003e:43\ndefined class Bank\nbank: org.apache.spark.sql.DataFrame \u003d [age: int, job: string ... 3 more fields]\nwarning: there were 1 deprecation warning(s); re-run with -deprecation for details\n"
}
]
},

29
pom.xml
View file

@ -78,6 +78,7 @@
<module>scio</module>
<module>zeppelin-web</module>
<module>zeppelin-server</module>
<module>zeppelin-jupyter</module>
<module>zeppelin-distribution</module>
</modules>
@ -105,6 +106,7 @@
<commons.io.version>2.4</commons.io.version>
<commons.collections.version>3.2.1</commons.collections.version>
<commons.logging.version>1.1.1</commons.logging.version>
<commons.cli.version>1.3.1</commons.cli.version>
<shiro.version>1.2.3</shiro.version>
<!-- test library versions -->
@ -232,6 +234,12 @@
<version>${commons.logging.version}</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>${commons.cli.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
@ -519,6 +527,27 @@
<version>${plugin.deploy.version}</version>
</plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>2.2.2</version>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
<configuration>
<skipPoms>false</skipPoms>
<dotGitDirectory>${project.basedir}/.git</dotGitDirectory>
<generateGitPropertiesFile>true</generateGitPropertiesFile>
<generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>
<failOnNoGitDirectory>false</failOnNoGitDirectory>
<dateFormat>yyyy-MM-dd HH:mm:ss</dateFormat>
</configuration>
</plugin>
<!--TODO(alex): make part of the build and reconcile conflicts
<plugin>
<groupId>com.ning.maven.plugins</groupId>

View file

@ -23,7 +23,6 @@ import java.util._
import org.apache.commons.codec.binary.{Base64, StringUtils}
import org.apache.zeppelin.interpreter.Interpreter.FormType
import org.apache.zeppelin.interpreter.remote.RemoteInterpreter
import org.apache.zeppelin.interpreter.{InterpreterContext, _}
import org.apache.zeppelin.scheduler.Scheduler
import org.apache.zeppelin.spark.SparkInterpreter

View file

@ -37,7 +37,7 @@ public class InterpreterOption {
boolean isExistingProcess;
boolean setPermission;
List<String> users;
List<String> owners;
boolean isUserImpersonate;
public boolean isExistingProcess() {
@ -64,8 +64,8 @@ public class InterpreterOption {
this.setPermission = setPermission;
}
public List<String> getUsers() {
return users;
public List<String> getOwners() {
return owners;
}
public boolean isUserImpersonate() {
@ -106,8 +106,8 @@ public class InterpreterOption {
option.perUser = other.perUser;
option.isExistingProcess = other.isExistingProcess;
option.setPermission = other.setPermission;
option.users = (null == other.users) ?
new ArrayList<String>() : new ArrayList<>(other.users);
option.owners = (null == other.owners) ?
new ArrayList<String>() : new ArrayList<>(other.owners);
return option;
}

View file

@ -50,7 +50,8 @@ public class InterpreterResult implements Serializable {
TABLE,
IMG,
SVG,
NULL
NULL,
NETWORK
}
Code code;

View file

@ -18,6 +18,10 @@
package org.apache.zeppelin.user;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -28,9 +32,10 @@ import org.slf4j.LoggerFactory;
public class AuthenticationInfo {
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationInfo.class);
String user;
List<String> roles;
String ticket;
UserCredentials userCredentials;
public static final AuthenticationInfo ANONYMOUS = new AuthenticationInfo("anonymous",
public static final AuthenticationInfo ANONYMOUS = new AuthenticationInfo("anonymous", null,
"anonymous");
public AuthenticationInfo() {}
@ -44,9 +49,13 @@ public class AuthenticationInfo {
* @param user
* @param ticket
*/
public AuthenticationInfo(String user, String ticket) {
public AuthenticationInfo(String user, String roles, String ticket) {
this.user = user;
this.ticket = ticket;
if (StringUtils.isNotBlank(roles) && roles.length() > 2) {
String[] r = roles.substring(1, roles.length() - 1).split(",");
this.roles = Arrays.asList(r);
}
}
public String getUser() {
@ -57,6 +66,26 @@ public class AuthenticationInfo {
this.user = user;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public List<String> getUsersAndRoles() {
List<String> usersAndRoles = new ArrayList<>();
if (roles != null) {
usersAndRoles.addAll(roles);
}
if (user != null) {
usersAndRoles.add(user);
}
return usersAndRoles;
}
public String getTicket() {
return ticket;
}

View file

@ -63,7 +63,7 @@ public class InterpreterTest {
null,
paragraphTitle,
paragraphText,
new AuthenticationInfo("testUser", "testTicket"),
new AuthenticationInfo("testUser", null, "testTicket"),
null,
null,
null,

64
zeppelin-jupyter/pom.xml Normal file
View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>zeppelin</artifactId>
<groupId>org.apache.zeppelin</groupId>
<version>0.8.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>zeppelin-jupyter</artifactId>
<packaging>jar</packaging>
<version>0.8.0-SNAPSHOT</version>
<name>Zeppelin: Jupyter Support</name>
<description>Jupyter support for Apache Zeppelin</description>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.danilopianini</groupId>
<artifactId>gson-extras</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,204 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.google.common.base.Joiner;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.zeppelin.jupyter.nbformat.Cell;
import org.apache.zeppelin.jupyter.nbformat.CodeCell;
import org.apache.zeppelin.jupyter.nbformat.DisplayData;
import org.apache.zeppelin.jupyter.nbformat.Error;
import org.apache.zeppelin.jupyter.nbformat.ExecuteResult;
import org.apache.zeppelin.jupyter.nbformat.HeadingCell;
import org.apache.zeppelin.jupyter.nbformat.MarkdownCell;
import org.apache.zeppelin.jupyter.nbformat.Nbformat;
import org.apache.zeppelin.jupyter.nbformat.Output;
import org.apache.zeppelin.jupyter.nbformat.RawCell;
import org.apache.zeppelin.jupyter.nbformat.Stream;
import org.apache.zeppelin.jupyter.zformat.Note;
import org.apache.zeppelin.jupyter.zformat.Paragraph;
import org.apache.zeppelin.jupyter.zformat.Result;
import org.apache.zeppelin.jupyter.zformat.TypeData;
/**
*
*/
public class JupyterUtil {
private static final String TEXT_PLAIN = "text/plain";
private static final String IMAGE_PNG = "image/png";
private final RuntimeTypeAdapterFactory<Cell> cellTypeFactory;
private final RuntimeTypeAdapterFactory<Output> outputTypeFactory;
public JupyterUtil() {
this.cellTypeFactory = RuntimeTypeAdapterFactory.of(Cell.class, "cell_type")
.registerSubtype(MarkdownCell.class, "markdown").registerSubtype(CodeCell.class, "code")
.registerSubtype(RawCell.class, "raw").registerSubtype(HeadingCell.class, "heading");
this.outputTypeFactory = RuntimeTypeAdapterFactory.of(Output.class, "output_type")
.registerSubtype(ExecuteResult.class, "execute_result")
.registerSubtype(DisplayData.class, "display_data").registerSubtype(Stream.class, "stream")
.registerSubtype(Error.class, "error");
}
public Nbformat getNbformat(Reader in) {
return getNbformat(in, new GsonBuilder());
}
public Nbformat getNbformat(Reader in, GsonBuilder gsonBuilder) {
return getGson(gsonBuilder).fromJson(in, Nbformat.class);
}
public Note getNote(Reader in, String codeReplaced, String markdownReplaced) {
return getNote(in, new GsonBuilder(), codeReplaced, markdownReplaced);
}
public Note getNote(Reader in, GsonBuilder gsonBuilder, String codeReplaced,
String markdownReplaced) {
return getNote(getNbformat(in, gsonBuilder), codeReplaced, markdownReplaced);
}
public Note getNote(Nbformat nbformat, String codeReplaced, String markdownReplaced) {
Note note = new Note();
String name = nbformat.getMetadata().getTitle();
if (null == name) {
name = "Note converted from Jupyter";
}
note.setName(name);
String lineSeparator = System.lineSeparator();
Paragraph paragraph;
List<Paragraph> paragraphs = new ArrayList<>();
String interpreterName;
List<TypeData> typeDataList;
String type;
String result;
for (Cell cell : nbformat.getCells()) {
paragraph = new Paragraph();
typeDataList = new ArrayList<>();
if (cell instanceof CodeCell) {
interpreterName = codeReplaced;
for (Output output : ((CodeCell) cell).getOutputs()) {
TypeData typeData;
if (output instanceof Stream) {
type = TypeData.TEXT;
result = Joiner.on(lineSeparator).join(((Stream) output).getText());
typeData = new TypeData(type, result);
typeDataList.add(typeData);
} else if (output instanceof ExecuteResult || output instanceof DisplayData) {
Map<String, Object> data = (output instanceof ExecuteResult) ?
((ExecuteResult) output).getData() :
((DisplayData) output).getData();
for (Map.Entry<String, Object> datum : data.entrySet()) {
if (TEXT_PLAIN.equals(datum.getKey())) {
type = TypeData.TEXT;
result = Joiner.on(lineSeparator).join((List<String>) datum.getValue());
} else if (IMAGE_PNG.equals(datum.getKey())) {
type = TypeData.HTML;
result = makeHTML(((String) datum.getValue()).replace("\n", ""));
} else {
type = TypeData.TEXT;
result = datum.getValue().toString();
}
typeData = new TypeData(type, result);
typeDataList.add(typeData);
}
} else {
// Error
Error error = (Error) output;
type = TypeData.TEXT;
result =
Joiner.on(lineSeparator).join(new String[] {error.getEname(), error.getEvalue()});
typeData = new TypeData(type, result);
typeDataList.add(typeData);
}
}
} else if (cell instanceof MarkdownCell || cell instanceof HeadingCell) {
interpreterName = markdownReplaced;
} else {
interpreterName = "";
}
paragraph.setText(
interpreterName + lineSeparator + Joiner.on(lineSeparator).join(cell.getSource()));
paragraph.setResults(new Result(Result.SUCCESS, typeDataList));
paragraphs.add(paragraph);
}
note.setParagraphs(paragraphs);
return note;
}
private Gson getGson(GsonBuilder gsonBuilder) {
return gsonBuilder.registerTypeAdapterFactory(cellTypeFactory)
.registerTypeAdapterFactory(outputTypeFactory).create();
}
private String makeHTML(String image) {
return "<div style='width:auto;height:auto'><img src=data:image/png;base64," + image
+ " style='width=auto;height:auto'/></div>";
}
public static void main(String[] args) throws ParseException, IOException {
Options options = new Options();
options.addOption("i", true, "Jupyter notebook file");
options.addOption("o", true, "Zeppelin note file. Default: note.json");
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
if (!cmd.hasOption("i")) {
new HelpFormatter().printHelp("java " + JupyterUtil.class.getName(), options);
System.exit(1);
}
Path jupyterPath = Paths.get(cmd.getOptionValue("i"));
Path zeppelinPath = Paths.get(cmd.hasOption("o") ? cmd.getOptionValue("o") : "note.json");
try (BufferedReader in = new BufferedReader(new FileReader(jupyterPath.toFile()));
FileWriter fw = new FileWriter(zeppelinPath.toFile())) {
Note note = new JupyterUtil().getNote(in, "python", "md");
Gson gson = new GsonBuilder().setPrettyPrinting().create();
gson.toJson(note, fw);
}
}
}

View file

@ -0,0 +1,29 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
/**
*
*/
public class Author {
@SerializedName("name")
private String name;
}

View file

@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
*
*/
public abstract class Cell {
@SerializedName("cell_type")
private String cellType;
@SerializedName("metadata")
private CellMetadata metadata;
@SerializedName("source")
private List<String> source;
public String getCellType() {
return cellType;
}
public CellMetadata getMetadata() {
return metadata;
}
public List<String> getSource() {
return source;
}
}

View file

@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
*
*/
public class CellMetadata {
@SerializedName("name")
private String name;
@SerializedName("tags")
private List<String> tags;
}

View file

@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
*
*/
public class CodeCell extends Cell {
@SerializedName("outputs")
private List<Output> outputs;
public List<Output> getOutputs() {
return outputs;
}
}

View file

@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
/**
*
*/
public class DisplayData extends Output {
@SerializedName("data")
private Map<String, Object> data;
public Map<String, Object> getData() {
return data;
}
}

View file

@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
*
*/
public class Error extends Output {
@SerializedName("ename")
private String ename;
@SerializedName("evalue")
private String evalue;
@SerializedName("traceback")
private List<String> traceback;
public String getEname() {
return ename;
}
public String getEvalue() {
return evalue;
}
public List<String> getTraceback() {
return traceback;
}
}

View file

@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
/**
*
*/
public class ExecuteResult extends Output {
@SerializedName("execution_count")
private int executionCount;
@SerializedName("data")
private Map<String, Object> data;
public Map<String, Object> getData() {
return data;
}
}

View file

@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
/**
*
*/
public class HeadingCell extends Cell {
@SerializedName("level")
private int level;
public int getLevel() {
return level;
}
}

View file

@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
/**
*
*/
public class Kernelspec {
@SerializedName("name")
private String name;
@SerializedName("display_name")
private String displayName;
}

View file

@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
/**
*
*/
public class LanguageInfo {
@SerializedName("name")
private String name;
@SerializedName("codemirror_mode")
private Object codemirrorMode;
@SerializedName("file_extension")
private String fileExtension;
@SerializedName("mimetype")
private String mimetype;
@SerializedName("pygments_lexer")
private String pygmentsLexer;
}

View file

@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
/**
*
*/
public class MarkdownCell extends Cell {
}

View file

@ -0,0 +1,61 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
*
*/
public class Metadata {
@SerializedName("kernelspec")
private Kernelspec kernelspec;
@SerializedName("language_info")
private LanguageInfo languageInfo;
@SerializedName("orig_nbformat")
private int origNbformat;
@SerializedName("title")
private String title;
@SerializedName("authors")
private List<Author> authors;
public Kernelspec getKernelspec() {
return kernelspec;
}
public LanguageInfo getLanguageInfo() {
return languageInfo;
}
public int getOrigNbformat() {
return origNbformat;
}
public String getTitle() {
return title;
}
public List<Author> getAuthors() {
return authors;
}
}

View file

@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
*
*/
public class Nbformat {
@SerializedName("metadata")
private Metadata metadata;
@SerializedName("nbformat")
private int nbformat;
@SerializedName("nbformat_minor")
private int nbformatMinor;
@SerializedName("cells")
private List<Cell> cells;
public Metadata getMetadata() {
return metadata;
}
public int getNbformat() {
return nbformat;
}
public int getNbformatMinor() {
return nbformatMinor;
}
public List<Cell> getCells() {
return cells;
}
}

View file

@ -0,0 +1,28 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
/**
*
*/
public abstract class Output {
@SerializedName("output_type")
private String outputType;
}

View file

@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
/**
*
*/
public class RawCell extends Cell {
}

View file

@ -0,0 +1,28 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
/**
*
*/
public class RawCellMetadata extends CellMetadata {
@SerializedName("format")
private String format;
}

View file

@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
*
*/
public class Stream extends Output {
@SerializedName("name")
private String name;
@SerializedName("text")
private List<String> text;
public List<String> getText() {
return text;
}
}

View file

@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.zformat;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
*
*/
public class Note {
@SerializedName("name")
private String name;
@SerializedName("paragraphs")
private List<Paragraph> paragraphs;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Paragraph> getParagraphs() {
return paragraphs;
}
public void setParagraphs(List<Paragraph> paragraphs) {
this.paragraphs = paragraphs;
}
}

View file

@ -0,0 +1,70 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.zformat;
import com.google.gson.annotations.SerializedName;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
*
*/
public class Paragraph {
public static final String FINISHED = "FINISHED";
@SerializedName("text")
private String text;
@SerializedName("results")
private Result results; // It's a bit weird name
@SerializedName("id")
private String id;
@SerializedName("status")
private String status;
public Paragraph() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss");
this.id = dateFormat.format(new Date()) + "_" + super.hashCode();
this.status = FINISHED;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Result getResults() {
return results;
}
public void setResults(Result results) {
this.results = results;
}
public String getId() {
return id;
}
public String getStatus() {
return status;
}
}

View file

@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.zformat;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
*
*/
public class Result {
public static final String SUCCESS = "SUCCESS";
@SerializedName("code")
private String code;
@SerializedName("msg")
private List<TypeData> msg;
public Result(String code, List<TypeData> msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public List<TypeData> getMsg() {
return msg;
}
}

View file

@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.zformat;
import com.google.gson.annotations.SerializedName;
/**
*
*/
public class TypeData {
public static final String TABLE = "TABLE";
public static final String HTML = "HTML";
public static final String TEXT = "TEXT";
@SerializedName("type")
private String type;
@SerializedName("data")
private String data;
public TypeData(String type, String data) {
this.type = type;
this.data = data;
}
public String getType() {
return type;
}
public String getData() {
return data;
}
}

View file

@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.jupyter.nbformat;
import static org.junit.Assert.assertTrue;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.apache.zeppelin.jupyter.JupyterUtil;
import org.apache.zeppelin.jupyter.zformat.Note;
import org.junit.Test;
/**
*
*/
public class JupyterUtilTest {
@Test
public void getNbFormat() {
InputStream resource = getClass().getResourceAsStream("/basic.ipynb");
Nbformat nbformat = new JupyterUtil().getNbformat(new InputStreamReader(resource));
assertTrue(nbformat.getCells().get(0) instanceof CodeCell);
resource = getClass().getResourceAsStream("/examples.ipynb");
nbformat = new JupyterUtil().getNbformat(new InputStreamReader(resource));
}
@Test
public void getNote() {
InputStream resource = getClass().getResourceAsStream("/examples.ipynb");
Note n = new JupyterUtil().getNote(new InputStreamReader(resource), "%python", "%md");
}
}

View file

@ -0,0 +1,149 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"import numpy as np"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# The first two lines of the A matrix represent the coordinates of each rotor in the X,Y plane,\n",
"# and the third line the direction in which they spin.\n",
"# Note that in this example the X axis is vertical, and the Y coordinates are in the top row of A.\n",
"A = np.array([[-0.17, 0.17, -0.25, 0.25, -0.33, 0.33],\n",
" [-0.35, -0.35, 0., 0., 0.35, 0.35],\n",
" [-0.1, 0.1, 0.1, -0.1, -0.1, 0.1 ]])"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Moore-Penrose pseudoinverse of A\n",
"B = np.linalg.pinv(A)\n",
"# normalize roll/pitch to the largest of both\n",
"# normalize yaw to 0.5\n",
"# and transpose\n",
"rp_max = B[:,0:1].max()\n",
"n = np.array([rp_max, rp_max, 2*B[:,2].max()])\n",
"B_nt = (B / n).T"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[ -67 67 -256 256 -189 189]\n",
" [-197 -197 0 0 197 197]\n",
" [ -77 77 128 -128 -57 57]]\n"
]
}
],
"source": [
"# scale and round to 256 to return final coefficients\n",
"scale = 256\n",
"coeffs = np.around(scale * B_nt).astype(int)\n",
"print(coeffs)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"<define name=\"ROLL_COEF\" value=\"{ -67, 67, -256, 256, -189, 189}\"/>\n",
"<define name=\"PITCH_COEF\" value=\"{-197, -197, 0, 0, 197, 197}\"/>\n",
"<define name=\"YAW_COEF\" value=\"{ -77, 77, 128, -128, -57, 57}\"/>\n"
]
}
],
"source": [
"# output defines\n",
"import string\n",
"rows = ['ROLL_COEF\" ', 'PITCH_COEF\" ', 'YAW_COEF\" ']\n",
"for i, r in enumerate(rows):\n",
" print('<define name=\"' + r + 'value=\"{' + string.join(['{:>4d}'.format(c) for c in coeffs[i]], ', ') + '}\"/>')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
""
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 2",
"language": "python",
"name": "python2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2.0
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 0
}

File diff suppressed because one or more lines are too long

View file

@ -297,6 +297,16 @@ public class ActiveDirectoryGroupRealm extends AbstractLdapRealm {
return userNameList;
}
public Map<String, String> getListRoles() {
Map<String, String> roles = new HashMap<>();
Iterator it = this.groupRolesMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry pair = (Map.Entry) it.next();
roles.put((String) pair.getValue(), "*");
}
return roles;
}
private Set<String> getRoleNamesForUser(String username, LdapContext ldapContext)
throws NamingException {
Set<String> roleNames = new LinkedHashSet<>();

View file

@ -101,6 +101,8 @@ import javax.naming.ldap.PagedResultsControl;
* # ability set searchScopes subtree (default), one, base
* ldapRealm.userSearchScope = subtree;
* ldapRealm.groupSearchScope = subtree;
* ldapRealm.userSearchFilter = (&(objectclass=person)(sAMAccountName={0}))
* ldapRealm.groupSearchFilter = (&(objectclass=groupofnames)(member={0}))
* ldapRealm.memberAttributeValueTemplate=cn={0},ou=people,dc=hadoop,dc=apache,
* dc=org
* # enable support for nested groups using the LDAP_MATCHING_RULE_IN_CHAIN operator
@ -160,6 +162,7 @@ public class LdapRealm extends JndiLdapRealm {
private Pattern principalPattern = Pattern.compile(DEFAULT_PRINCIPAL_REGEX);
private String userDnTemplate = "{0}";
private String userSearchFilter = null;
private String groupSearchFilter = null;
private String userSearchAttributeTemplate = "{0}";
private String userSearchScope = "subtree";
private String groupSearchScope = "subtree";
@ -356,9 +359,22 @@ public class LdapRealm extends JndiLdapRealm {
}
}
} else {
// Default group search filter
String searchFilter = String.format("(objectclass=%1$s)", groupObjectClass);
// If group search filter is defined in Shiro config, then use it
if (groupSearchFilter != null) {
Matcher matchedPrincipal = matchPrincipal(userDn);
searchFilter = expandTemplate(groupSearchFilter, matchedPrincipal);
//searchFilter = String.format("%1$s", groupSearchFilter);
}
if (log.isDebugEnabled()) {
log.debug("Group SearchBase|SearchFilter|GroupSearchScope: " + getGroupSearchBase()
+ "|" + searchFilter + "|" + groupSearchScope);
}
searchResultEnum = ldapCtx.search(
getGroupSearchBase(),
"objectClass=" + groupObjectClass,
searchFilter,
searchControls);
while (searchResultEnum != null && searchResultEnum.hasMore()) {
// searchResults contains all the groups in search scope
@ -737,6 +753,14 @@ public class LdapRealm extends JndiLdapRealm {
this.userSearchFilter = (filter == null ? null : filter.trim());
}
public String getGroupSearchFilter() {
return groupSearchFilter;
}
public void setGroupSearchFilter(final String filter) {
this.groupSearchFilter = (filter == null ? null : filter.trim());
}
public boolean getUserLowerCase() {
return userLowerCase;
}

View file

@ -23,6 +23,9 @@ import org.apache.zeppelin.annotation.ZeppelinApi;
import org.apache.zeppelin.server.JsonResponse;
import org.apache.zeppelin.util.Util;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
@ -56,7 +59,12 @@ public class ZeppelinRestApi {
@Path("version")
@ZeppelinApi
public Response getVersion() {
return new JsonResponse<>(Response.Status.OK, "Zeppelin version", Util.getVersion()).build();
Map<String, String> versionInfo = new HashMap<>();
versionInfo.put("version", Util.getVersion());
versionInfo.put("git-commit-id", Util.getGitCommitId());
versionInfo.put("git-timestamp", Util.getGitTimestamp());
return new JsonResponse<>(Response.Status.OK, "Zeppelin version", versionInfo).build();
}
/**

View file

@ -226,7 +226,8 @@ public class NotebookServer extends WebSocketServlet
addUserConnection(messagereceived.principal, conn);
}
AuthenticationInfo subject =
new AuthenticationInfo(messagereceived.principal, messagereceived.ticket);
new AuthenticationInfo(messagereceived.principal, messagereceived.roles,
messagereceived.ticket);
/** Lets be elegant here */
switch (messagereceived.op) {
@ -1807,7 +1808,7 @@ public class NotebookServer extends WebSocketServlet
p.setText(text);
p.setTitle(title);
AuthenticationInfo subject =
new AuthenticationInfo(fromMessage.principal, fromMessage.ticket);
new AuthenticationInfo(fromMessage.principal, fromMessage.roles, fromMessage.ticket);
p.setAuthenticationInfo(subject);
p.settings.setParams(params);
p.setConfig(config);

View file

@ -34,6 +34,7 @@ import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.realm.ActiveDirectoryGroupRealm;
import org.apache.zeppelin.realm.LdapRealm;
import org.mortbay.log.Log;
import org.slf4j.Logger;
@ -133,6 +134,9 @@ public class SecurityUtils {
} else if (name.equals("org.apache.zeppelin.realm.LdapRealm")) {
allRoles = ((LdapRealm) realm).getListRoles();
break;
} else if (name.equals("org.apache.zeppelin.realm.ActiveDirectoryGroupRealm")) {
allRoles = ((ActiveDirectoryGroupRealm) realm).getListRoles();
break;
}
}
if (allRoles != null) {

View file

@ -262,7 +262,7 @@ public class ParagraphActionsIT extends AbstractZeppelinIT {
}
try {
String xpathToRunOnSelectionChangeCheckbox = getParagraphXPath(1) + "//ul/li/form/input[contains(@ng-checked, 'true')]";
String xpathToDropdownMenu = "(" + (getParagraphXPath(1) + "//select)[2]");
String xpathToDropdownMenu = getParagraphXPath(1) + "//select";
String xpathToResultText = getParagraphXPath(1) + "//div[contains(@id,\"_html\")]";
createNewNote();
@ -592,7 +592,7 @@ public class ParagraphActionsIT extends AbstractZeppelinIT {
driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(),
CoreMatchers.equalTo("Howdy "));
Select dropDownMenu = new Select(driver.findElement(By.xpath("(" + (getParagraphXPath(1) + "//select)[2]"))));
Select dropDownMenu = new Select(driver.findElement(By.xpath("(" + (getParagraphXPath(1) + "//select)[1]"))));
dropDownMenu.selectByVisibleText("Alice");
collector.checkThat("After selection in drop down menu, output should display the newly selected option",
@ -602,7 +602,7 @@ public class ParagraphActionsIT extends AbstractZeppelinIT {
driver.findElement(By.xpath(getParagraphXPath(1) + "//span[@class='icon-settings']")).click();
clickAndWait(By.xpath(getParagraphXPath(1) + "//ul/li/form/input[contains(@ng-checked, 'true')]"));
Select sameDropDownMenu = new Select(driver.findElement(By.xpath("(" + (getParagraphXPath(1) + "//select)[2]"))));
Select sameDropDownMenu = new Select(driver.findElement(By.xpath("(" + (getParagraphXPath(1) + "//select)[1]"))));
sameDropDownMenu.selectByVisibleText("Bob");
collector.checkThat("After 'Run on selection change' checkbox is unchecked, the paragraph should not run if selecting a different option",
driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(),
@ -632,7 +632,7 @@ public class ParagraphActionsIT extends AbstractZeppelinIT {
driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(),
CoreMatchers.containsString("Greetings han and leia and luke"));
WebElement firstCheckbox = driver.findElement(By.xpath("(" + getParagraphXPath(1) + "//input[@type='checkbox'])[2]"));
WebElement firstCheckbox = driver.findElement(By.xpath("(" + getParagraphXPath(1) + "//input[@type='checkbox'])[1]"));
firstCheckbox.click();
collector.checkThat("After unchecking one of the boxes, we can see the newly updated output without the option we unchecked",
driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(),
@ -641,7 +641,7 @@ public class ParagraphActionsIT extends AbstractZeppelinIT {
driver.findElement(By.xpath(getParagraphXPath(1) + "//span[@class='icon-settings']")).click();
clickAndWait(By.xpath(getParagraphXPath(1) + "//ul/li/form/input[contains(@ng-checked, 'true')]"));
WebElement secondCheckbox = driver.findElement(By.xpath("(" + getParagraphXPath(1) + "//input[@type='checkbox'])[3]"));
WebElement secondCheckbox = driver.findElement(By.xpath("(" + getParagraphXPath(1) + "//input[@type='checkbox'])[2]"));
secondCheckbox.click();
collector.checkThat("After 'Run on selection change' checkbox is unchecked, the paragraph should not run if check box state is modified",
driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(),
@ -676,7 +676,7 @@ public class ParagraphActionsIT extends AbstractZeppelinIT {
driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(),
CoreMatchers.equalTo("Howdy \nHowdy "));
Select dropDownMenu = new Select(driver.findElement(By.xpath("(" + (getParagraphXPath(1) + "//select)[2]"))));
Select dropDownMenu = new Select(driver.findElement(By.xpath("(" + (getParagraphXPath(1) + "//select)[1]"))));
dropDownMenu.selectByVisibleText("Apple");
collector.checkThat("After selection in drop down menu, output should display the new option we selected",
driver.findElement(By.xpath(getParagraphXPath(1) + "//div[contains(@class, 'text plainTextContent')]")).getText(),
@ -685,7 +685,7 @@ public class ParagraphActionsIT extends AbstractZeppelinIT {
driver.findElement(By.xpath(getParagraphXPath(1) + "//span[@class='icon-settings']")).click();
clickAndWait(By.xpath(getParagraphXPath(1) + "//ul/li/form/input[contains(@ng-checked, 'true')]"));
Select sameDropDownMenu = new Select(driver.findElement(By.xpath("(" + (getParagraphXPath(1) + "//select)[3]"))));
Select sameDropDownMenu = new Select(driver.findElement(By.xpath("(" + (getParagraphXPath(1) + "//select)[2]"))));
sameDropDownMenu.selectByVisibleText("Earth");
waitForParagraph(1, "FINISHED");
collector.checkThat("After 'Run on selection change' checkbox is unchecked, the paragraph should not run if selecting a different option",

View file

@ -193,7 +193,7 @@ public class SparkParagraphIT extends AbstractZeppelinIT {
}
collector.checkThat("Paragraph from SparkParagraphIT of testSqlSpark result: ",
headerNames, CoreMatchers.equalTo("Age|Job|Marital|Education|Balance|"));
headerNames, CoreMatchers.equalTo("age|job|marital|education|balance|"));
} catch (Exception e) {
handleException("Exception in SparkParagraphIT while testSqlSpark", e);
}

View file

@ -167,7 +167,7 @@ public class ZeppelinRestApiTest extends AbstractTestRestApi {
assertEquals("paragraph col width check failed", 9.0, p.getConfig().get("colWidth"));
assertTrue("paragraph show title check failed", ((boolean) p.getConfig().get("title")));
Map graph = ((List<Map>)p.getConfig().get("results")).get(0);
String mode = graph.get("mode").toString();
String mode = ((Map)graph.get("graph")).get("mode").toString();
assertEquals("paragraph graph mode check failed", "pieChart", mode);
}
}

View file

@ -58,6 +58,7 @@
"no-undef": 2,
"no-unused-vars": [2, { "vars": "local", "args": "none" }],
"strict": [2, "global"],
"max-len": [2, {"code": 120, "ignoreComments": true, "ignoreRegExpLiterals": true}]
"max-len": [2, {"code": 120, "ignoreComments": true, "ignoreRegExpLiterals": true}],
"linebreak-style": 0
}
}

View file

@ -105,7 +105,7 @@ let zeppelinWebApp = angular.module('zeppelinWebApp', requiredModules)
})
.when('/jobmanager', {
templateUrl: 'app/jobmanager/jobmanager.html',
controller: 'JobmanagerCtrl'
controller: 'JobManagerCtrl'
})
.when('/interpreter', {
templateUrl: 'app/interpreter/interpreter.html',

View file

@ -252,13 +252,13 @@ limitations under the License.
<div ng-show="newInterpreterSetting.option.setPermission" class="permissionsForm">
<div>
<p>
Enter comma separated users in the fields. <br />
Enter comma separated users and groups in the fields. <br />
Empty field (*) implies anyone can run this interpreter.
</p>
<div>
<span class="owners">Owners </span>
<select id="newInterpreterUsers" class="form-control" multiple="multiple">
<option ng-repeat="user in newInterpreterSetting.option.users" selected="selected">{{user}}</option>
<select id="newInterpreterOwners" class="form-control" multiple="multiple">
<option ng-repeat="owner in newInterpreterSetting.option.owners" selected="selected">{{owner}}</option>
</select>
</div>
</div>

View file

@ -39,10 +39,10 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou
let getSelectJson = function () {
let selectJson = {
tags: false,
tags: true,
minimumInputLength: 3,
multiple: true,
tokenSeparators: [',', ' '],
minimumInputLength: 2,
ajax: {
url: function (params) {
if (!params.term) {
@ -52,17 +52,36 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou
},
delay: 250,
processResults: function (data, params) {
let users = []
let results = []
if (data.body.users.length !== 0) {
for (let i = 0; i < data.body.users.length; i++) {
let users = []
for (let len = 0; len < data.body.users.length; len++) {
users.push({
'id': data.body.users[i],
'text': data.body.users[i]
'id': data.body.users[len],
'text': data.body.users[len]
})
}
results.push({
'text': 'Users :',
'children': users
})
}
if (data.body.roles.length !== 0) {
let roles = []
for (let len = 0; len < data.body.roles.length; len++) {
roles.push({
'id': data.body.roles[len],
'text': data.body.roles[len]
})
}
results.push({
'text': 'Roles :',
'children': roles
})
}
return {
results: users,
results: results,
pagination: {
more: false
}
@ -75,7 +94,7 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou
}
$scope.togglePermissions = function (intpName) {
angular.element('#' + intpName + 'Users').select2(getSelectJson())
angular.element('#' + intpName + 'Owners').select2(getSelectJson())
if ($scope.showInterpreterAuth) {
$scope.closePermissions()
} else {
@ -85,7 +104,7 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou
$scope.$on('ngRenderFinished', function (event, data) {
for (let setting = 0; setting < $scope.interpreterSettings.length; setting++) {
angular.element('#' + $scope.interpreterSettings[setting].name + 'Users').select2(getSelectJson())
angular.element('#' + $scope.interpreterSettings[setting].name + 'Owners').select2(getSelectJson())
}
})
@ -359,7 +378,7 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou
// remote always true for now
setting.option.remote = true
}
setting.option.users = angular.element('#' + setting.name + 'Users').val()
setting.option.owners = angular.element('#' + setting.name + 'Owners').val()
let request = {
option: angular.copy(setting.option),
@ -498,7 +517,7 @@ function InterpreterCtrl($rootScope, $scope, $http, baseUrlSrv, ngToast, $timeou
if (newSetting.option.setPermission === undefined) {
newSetting.option.setPermission = false
}
newSetting.option.users = angular.element('#newInterpreterUsers').val()
newSetting.option.owners = angular.element('#newInterpreterOwners').val()
let request = angular.copy($scope.newInterpreterSetting)

View file

@ -372,14 +372,14 @@ limitations under the License.
<div ng-show="setting.option.setPermission" class="permissionsForm">
<div>
<p>
Enter comma separated users in the fields. <br />
Enter comma separated users and groups in the fields. <br />
Empty field (*) implies anyone can run this interpreter.
</p>
<div>
<span class="owners">Owners </span>
<select id="{{setting.name}}Users" class="form-control" multiple="multiple" ng-disabled="!valueform.$visible">
<option ng-repeat="user in setting.option.users" selected="selected">{{user}}</option>
<select id="{{setting.name}}Owners" class="form-control" multiple="multiple" ng-disabled="!valueform.$visible">
<option ng-repeat="owner in setting.option.owners" selected="selected">{{owner}}</option>
</select>
</div>
</div>

View file

@ -12,12 +12,40 @@
* limitations under the License.
*/
angular.module('zeppelinWebApp')
.controller('JobmanagerCtrl', JobmanagerCtrl)
import { JobStatus, } from './jobs/job-status'
function JobmanagerCtrl ($scope, websocketMsgSrv, $interval, ngToast, $q, $timeout, jobManagerFilter) {
angular.module('zeppelinWebApp')
.controller('JobManagerCtrl', JobManagerCtrl)
const JobDateSorter = {
RECENTLY_UPDATED: 'Recently Update',
OLDEST_UPDATED: 'Oldest Updated',
}
function JobManagerCtrl ($scope, websocketMsgSrv, $interval, ngToast, $q, $timeout, jobManagerFilter) {
'ngInject'
$scope.pagination = {
currentPage: 1,
itemsPerPage: 10,
maxPageCount: 5,
}
$scope.sorter = {
AvailableDateSorter: Object.keys(JobDateSorter).map(key => { return JobDateSorter[key] }),
currentDateSorter: JobDateSorter.RECENTLY_UPDATED,
}
$scope.setJobDateSorter = function(dateSorter) {
$scope.sorter.currentDateSorter = dateSorter
}
$scope.getJobsInCurrentPage = function(jobs) {
const cp = $scope.pagination.currentPage
const itp = $scope.pagination.itemsPerPage
return jobs.slice((cp - 1) * itp, (cp * itp))
}
ngToast.dismiss()
let asyncNotebookJobFilter = function (jobInfomations, filterConfig) {
return $q(function (resolve, reject) {
@ -26,15 +54,52 @@ function JobmanagerCtrl ($scope, websocketMsgSrv, $interval, ngToast, $q, $timeo
})
}
$scope.$watch('sorter.currentDateSorter', function() {
$scope.filterConfig.isSortByAsc =
$scope.sorter.currentDateSorter === JobDateSorter.OLDEST_UPDATED
asyncNotebookJobFilter($scope.jobInfomations, $scope.filterConfig)
})
$scope.getJobIconByStatus = function(jobStatus) {
if (jobStatus === JobStatus.READY) {
return 'fa fa-circle-o'
} else if (jobStatus === JobStatus.FINISHED) {
return 'fa fa-circle'
} else if (jobStatus === JobStatus.ABORT) {
return 'fa fa-circle'
} else if (jobStatus === JobStatus.ERROR) {
return 'fa fa-circle'
} else if (jobStatus === JobStatus.PENDING) {
return 'fa fa-circle'
} else if (jobStatus === JobStatus.RUNNING) {
return 'fa fa-spinner'
}
}
$scope.getJobColorByStatus = function(jobStatus) {
if (jobStatus === JobStatus.READY) {
return 'green'
} else if (jobStatus === JobStatus.FINISHED) {
return 'green'
} else if (jobStatus === JobStatus.ABORT) {
return 'orange'
} else if (jobStatus === JobStatus.ERROR) {
return 'red'
} else if (jobStatus === JobStatus.PENDING) {
return 'gray'
} else if (jobStatus === JobStatus.RUNNING) {
return 'blue'
}
}
$scope.doFiltering = function (jobInfomations, filterConfig) {
asyncNotebookJobFilter(jobInfomations, filterConfig).then(
function () {
// success
$scope.isLoadingFilter = false
},
function () {
// failed
})
asyncNotebookJobFilter(jobInfomations, filterConfig)
.then(
() => { $scope.isLoadingFilter = false },
(error) => {
console.error('Failed to search jobs from server', error)
}
)
}
$scope.filterValueToName = function (filterValue, maxStringLength) {
@ -48,7 +113,7 @@ function JobmanagerCtrl ($scope, websocketMsgSrv, $interval, ngToast, $q, $timeo
}
return $scope.activeInterpreters[index].name
} else {
return 'Interpreter is not set'
return 'NONE'
}
}
@ -57,37 +122,6 @@ function JobmanagerCtrl ($scope, websocketMsgSrv, $interval, ngToast, $q, $timeo
$scope.doFiltering($scope.jobInfomations, $scope.filterConfig)
}
$scope.onChangeRunJobToAlwaysTopToggle = function () {
$scope.filterConfig.isRunningAlwaysTop = !$scope.filterConfig.isRunningAlwaysTop
$scope.doFiltering($scope.jobInfomations, $scope.filterConfig)
}
$scope.onChangeSortAsc = function () {
$scope.filterConfig.isSortByAsc = !$scope.filterConfig.isSortByAsc
$scope.doFiltering($scope.jobInfomations, $scope.filterConfig)
}
$scope.doFilterInputTyping = function (keyEvent, jobInfomations, filterConfig) {
let RETURN_KEY_CODE = 13
$timeout.cancel($scope.dofilterTimeoutObject)
$scope.isActiveSearchTimer = true
$scope.dofilterTimeoutObject = $timeout(function () {
$scope.doFiltering(jobInfomations, filterConfig)
$scope.isActiveSearchTimer = false
}, 10000)
if (keyEvent.which === RETURN_KEY_CODE) {
$timeout.cancel($scope.dofilterTimeoutObject)
$scope.doFiltering(jobInfomations, filterConfig)
$scope.isActiveSearchTimer = false
}
}
$scope.doForceFilterInputTyping = function (keyEvent, jobInfomations, filterConfig) {
$timeout.cancel($scope.dofilterTimeoutObject)
$scope.doFiltering(jobInfomations, filterConfig)
$scope.isActiveSearchTimer = false
}
$scope.init = function () {
$scope.isLoadingFilter = true
$scope.jobInfomations = []
@ -96,21 +130,13 @@ function JobmanagerCtrl ($scope, websocketMsgSrv, $interval, ngToast, $q, $timeo
isRunningAlwaysTop: true,
filterValueNotebookName: '',
filterValueInterpreter: '*',
isSortByAsc: true
isSortByAsc: $scope.sorter.currentDateSorter === JobDateSorter.OLDEST_UPDATED,
}
$scope.sortTooltipMsg = 'Switch to sort by desc'
$scope.jobTypeFilter = jobManagerFilter
websocketMsgSrv.getNoteJobsList()
$scope.$watch('filterConfig.isSortByAsc', function (value) {
if (value) {
$scope.sortTooltipMsg = 'Switch to sort by desc'
} else {
$scope.sortTooltipMsg = 'Switch to sort by asc'
}
})
$scope.$on('$destroy', function () {
websocketMsgSrv.unsubscribeJobManager()
})

View file

@ -22,7 +22,7 @@
min-height: 32px;
}
.jobManagerHead {
.job-manager-header {
margin: -10px -10px 20px;
padding: 10px 15px 15px 15px;
background: white;
@ -30,18 +30,88 @@
border-bottom: 1px solid #E5E5E5;
}
.jobManagerHead .header {
.job-manager-header .header {
font-family: 'Roboto', sans-serif;
}
.job-note-name-query {
padding: 6px;
height: 25px;
width: 200px;
.job-search-tool {
display: inline-block;
margin-left: 15px;
}
.job-note-name-font-family {
font: inherit;
font-size: 14px;
font-weight: normal;
#job-manager-header .job-search-tool .search-input {
margin-right: 7px;
min-width: 215px;
}
#job-manager-header .job-search-tool .search-input > input {
font-family: 'FontAwesome', 'Helvetica Neue', Helvetica, Arial, sans-serif;
border-radius: 4px;
font-size: 14px;
padding-left: 10px;
}
#job-manager-header .job-search-tool .dropdown-toggle {
border-radius: 3px;
float: none;
min-width: 150px;
text-align: left;
margin-right: 5px;
}
#job-manager-header .job-search-tool .date-sort-button {
min-width: 180px;
}
.job-search-tool .dropdown-text-desc {
color: gray;
font-weight: 400;
font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.job-search-tool .dropdown-text-value {
margin-left: 2px;
font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.search-tool-dropdown-content > li > a {
border-bottom: 1px solid #eee;
}
.search-tool-dropdown-content > li:last-child > a {
border-bottom: none;
}
.job-icon-desc-container {
display: inline-block;
margin-top: 10px;
margin-right: 30px;
float: right;
}
.job-desc-icon {
margin-right: 3px;
}
.job-pagination-container {
text-align: center;
margin-top: 50px;
padding-bottom: 100px;
}
.job-counter {
float: right;
clear: both;
margin-left: 8px;
margin-top: 6px;
}
.job-counter .job-counter-label {
color: gray;
}
.job-counter .job-counter-value {
font-size: 15px;
font-weight: bold;
}

View file

@ -22,26 +22,34 @@ function jobManagerFilter () {
let filterItems = jobItems
if (filterValueInterpreter === undefined) {
filterItems = _.filter(filterItems, function (jobItem) {
return jobItem.interpreter === undefined ? true : false
filterItems = filterItems.filter((jobItem) => {
return jobItem.interpreter === undefined
})
} else if (filterValueInterpreter !== '*') {
filterItems = _.where(filterItems, {interpreter: filterValueInterpreter})
}
if (filterValueNotebookName !== '') {
filterItems = _.filter(filterItems, function (jobItem) {
filterItems = filterItems.filter((jobItem) => {
let lowerFilterValue = filterValueNotebookName.toLocaleLowerCase()
let lowerNotebookName = jobItem.noteName.toLocaleLowerCase()
return lowerNotebookName.match(new RegExp('.*' + lowerFilterValue + '.*'))
})
}
filterItems = _.sortBy(filterItems, function (sortItem) {
return sortItem.noteName.toLowerCase()
filterItems = filterItems.sort((jobItem) => {
return jobItem.noteName.toLowerCase()
})
return isSortByAsc ? filterItems : filterItems.reverse()
filterItems = filterItems.sort((x, y) => {
if (isSortByAsc) {
return x.unixTimeLastRun - y.unixTimeLastRun
} else {
return y.unixTimeLastRun - x.unixTimeLastRun
}
})
return filterItems
}
return filterContext
}

View file

@ -11,8 +11,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Here the controller <JobmanagerCtrl> is not needed because explicitly set in the app.js (route) -->
<div class="jobManagerHead" data-ng-init="init()">
<!-- Here the controller <JobManagerCtrl> is not needed because explicitly set in the app.js (route) -->
<div id="job-manager-header" class="job-manager-header" data-ng-init="init()">
<div class="header">
<div class="row">
<div class="col-md-12">
@ -27,138 +27,117 @@ limitations under the License.
</div>
</div>
</div>
<div style="margin: 0px">
<hr style="margin-top: 10px; margin-bottom: 10px;" />
</div>
<hr style="margin-top: 15px; margin-bottom: 15px;" />
<div class="row">
<div class="col-md-6 text-left">
<!-- search tools (input, drop-down, sorting) -->
<div class="job-search-tool">
<div class="form-inline">
<span class="labelBtn btn-group">
<button type="button"
class="btn btn-default"
style="width: 25px; height: 25px; margin-right: 0px; padding: 1px 0px 3px 3px"
ng-click="onChangeSortAsc()"
tooltip-placement="right" uib-tooltip="{{sortTooltipMsg}}">
<i class="fa" ng-class="{true: 'fa-sort-amount-asc', false : 'fa-sort-amount-desc'}[filterConfig.isSortByAsc]"></i>
</button>
</span>
<span class="labelBtn btn-group" style="margin-left: 0px; padding-left: 3px;">
<button type="button" class="btn btn-default btn-xs dropdown-toggle"
data-toggle="dropdown"
style="min-width: 100px; text-align: right !important;">
<span class="text-right job-note-name-font-family">
{{filterValueToName(filterConfig.filterValueInterpreter)}}<span class="caret" style="margin-left: 10px"></span>
</span>
</button>
<ul class="dropdown-menu pull-left" role="menu">
<span class="btn-group">
<!-- search tool: input -->
<div class="input-group search-input">
<input class="form-control btn-xs"
placeholder="&#xf002 Search jobs..."
type="text"
ng-model="filterConfig.filterValueNotebookName"
ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 300, 'blur': 0 } }"
ng-change="doFiltering(jobInfomations, filterConfig)" />
</div>
<!-- search tool: default interpreter dropdown -->
<div class="btn btn-default dropdown-toggle"
data-toggle="dropdown">
<span>
<span class="dropdown-text-desc">Interpreter: </span>
<span class="dropdown-text-value">{{filterValueToName(filterConfig.filterValueInterpreter)}}</span>
<span class="caret" style="margin-top: 8px; float: right;"></span>
<span style="clear: both;"></span>
</span>
</div>
<ul class="dropdown-menu dropdown-menu-right search-tool-dropdown-content" role="menu">
<li ng-repeat="interpreterOption in activeInterpreters">
<a class="job-note-name-font-family" style="cursor:pointer"
ng-click="setFilterValue(interpreterOption.value)">
<a ng-click="setFilterValue(interpreterOption.value)"
ng-style="(filterValueToName(interpreterOption.value) === 'ALL' || filterValueToName(interpreterOption.value) === 'NONE') ? { 'font-weight': 500 } : {}"
class="dropdown-list-value">
{{filterValueToName(interpreterOption.value)}}
</a>
</li>
</ul>
<div class="input-group">
<input
class="job-note-name-query job-note-name-font-family form-control btn-xs"
style="margin-left: 5px;"
placeholder="Search for Job"
type="text" ng-model="filterConfig.filterValueNotebookName"
ng-keyup="doFilterInputTyping($event, jobInfomations, filterConfig, isLoadingFilter)"/>
<span
class="input-group-addon text-right" ng-class="{true : 'btn-primary active', false: ''}[isActiveSearchTimer]"
style="height: 5px; padding: 0px 6px 0px 9px !important;"
ng-click="doForceFilterInputTyping($event, jobInfomations, filterConfig, isLoadingFilter)">
<i class="fa fa-search fa-sm"></i></span>
</div>
</span>
<span class="btn-group">
<!-- search tool: date dropdown -->
<div class="date-sort-button btn btn-default dropdown-toggle" data-toggle="dropdown">
<span>
<span class="dropdown-text-desc">Sort: </span>
<span class="dropdown-text-value">{{sorter.currentDateSorter}}</span>
<span class="caret" style="margin-top: 8px; float: right;"></span>
<span style="clear: both;"></span>
</span>
</div>
<ul class="dropdown-menu dropdown-menu-right search-tool-dropdown-content" role="menu">
<li ng-repeat="dateSorter in sorter.AvailableDateSorter">
<a ng-click="setJobDateSorter(dateSorter)" class="dropdown-list-value">
{{dateSorter}}
</a>
</li>
</ul>
</span>
<span class="job-counter">
<span class="job-counter-label">Total: </span>
<span class="job-counter-value">{{JobInfomationsByFilter.length}}</span>
</span>
</div>
</div>
<div
class="col-md-6 text-right"
style="padding-top: 6px;">
<span
ng-repeat="jobStatus in ['READY', 'FINISHED', 'ABORT', 'ERROR','PENDING','RUNNING']"
ng-switch="jobStatus">
<span
ng-switch-when="FINISHED">
<i style="color: green; margin-right: 3px;" class="fa fa-circle"
ng-click="">
</i>
{{jobStatus}}
</span>
<span
ng-switch-when="RUNNING"
style="margin-right: 3px;">
<i style="color: blue" class="fa fa-spinner"
ng-click="">
</i>
{{jobStatus}}
</span>
<span
ng-switch-when="READY"
style="margin-right: 3px;">
<i style="color: green" class="fa fa-circle-o"
ng-click="">
</i>
{{jobStatus}}
</span>
<span
ng-switch-when="PENDING"
style="margin-right: 3px;">
<i style="color: gray" class="fa fa-circle"
ng-click="">
</i>
{{jobStatus}}
</span>
<span
ng-switch-when="ABORT"
style="margin-right: 3px;">
<i style="color: orange" class="fa fa-circle"
ng-click="">
</i>
{{jobStatus}}
</span>
<span
ng-switch-when="ERROR"
style="margin-right: 3px;">
<i style="color: red" class="fa fa-circle"
ng-click="">
</i>
{{jobStatus}}
<!-- job icon descriptions -->
<div class="job-icon-desc-container hidden-xs hidden-sm hidden-md">
<span ng-repeat="jobStatus in ['READY', 'FINISHED', 'ABORT', 'ERROR','PENDING','RUNNING']">
<span style="margin-right: 2px;">
<i class="job-desc-icon"
ng-style="{'color': getJobColorByStatus(jobStatus)}"
ng-class="getJobIconByStatus(jobStatus)" ></i>{{jobStatus}}
</span>
</span>
</div>
<div style="clear: both;"></div>
</div>
</div>
<div>
<div class="note-jump"></div>
<div
ng-if="isLoadingFilter === true"
class="paragraph-col">
<div
class="job-space box job-margin text-center">
<i style="color: blue" class="fa fa-spinner spinAnimation">
</i>&nbsp;Loading...
<div ng-if="isLoadingFilter === true" class="paragraph-col">
<div class="job-space box job-margin text-center">
<i style="color: blue" class="fa fa-spinner spinAnimation"></i>Loading...
</div>
</div>
<div
ng-if="JobInfomationsByFilter.length > 0"
ng-repeat="notebookJob in JobInfomationsByFilter track by $index"
class="paragraph-col">
<div ng-if="JobInfomationsByFilter.length > 0"
ng-repeat="notebookJob in getJobsInCurrentPage(JobInfomationsByFilter)"
class="paragraph-col">
<div ng-include src="'app/jobmanager/jobs/job.html'"
class="job-space box job-margin"
ng-controller="JobCtrl">
</div>
</div>
<div
ng-if="isLoadingFilter === false && JobInfomationsByFilter.length <= 0"
class="paragraph-col">
<div
class="job-space box job-margin text-center">
No Job found
</div>
<div ng-if="isLoadingFilter === false && JobInfomationsByFilter.length <= 0"
class="paragraph-col">
<div class="job-space box job-margin text-center">No Job found</div>
</div>
<div style="clear:both;height:10px"></div>
<!-- pagination -->
<div class="job-pagination-container">
<ul uib-pagination class="pagination-sm"
total-items="JobInfomationsByFilter.length"
ng-model="pagination.currentPage"
items-per-page="pagination.itemsPerPage"
boundary-links="true" rotate="false"
max-size="pagination.maxPageCount"
previous-text="&lsaquo;" next-text="&rsaquo;"
first-text="&laquo;" last-text="&raquo;"></ul>
</div>
</div>

View file

@ -0,0 +1,22 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const JobStatus = {
READY: 'READY',
FINISHED: 'FINISHED',
ABORT: 'ABORT',
ERROR: 'ERROR',
PENDING: 'PENDING',
RUNNING: 'RUNNING',
}

View file

@ -15,89 +15,28 @@ limitations under the License.
<div class="job" data-ng-init="init(notebookJob)">
<div>
<div ng-include src="'app/jobmanager/jobs/job-control.html'"></div>
<span
class="job-types"
ng-switch="notebookJob.noteType">
<span class="job-types"
ng-switch="notebookJob.noteType">
<i ng-switch-when="normal" class="icon-doc"></i>
<i ng-switch-when="cron" class="icon-clock"></i>
<i ng-switch-default class="icon-question"></i>
</span>
&nbsp;
<a style="text-decoration: none !important;" ng-href="#/notebook/{{notebookJob.noteId}}">
<span>
{{notebookJob.noteName}}
</span>
<span>
&nbsp;-&nbsp;
</span>
<span>
<span ng-if="notebookJob.interpreter === undefined" style="color: orange">
Interpreter is not set
</span>
<span ng-if="notebookJob.interpreter !== undefined" style="color: gray">
{{notebookJob.interpreter}}
</span>
</span>
<span>{{notebookJob.noteName}} - </span>
<span ng-if="notebookJob.interpreter === undefined" style="color: gray;">
interpreter is not set</span>
<span ng-if="notebookJob.interpreter !== undefined" style="color: black;">
{{notebookJob.interpreter}}</span>
</a>
<div ng-include src="'app/jobmanager/jobs/job-progressBar.html'"></div>
</div>
<div>
<span
ng-repeat="paragraphJob in notebookJob.paragraphs"
ng-switch="paragraphJob.status">
<a ng-switch-when="READY"
style="text-decoration: none !important;"
<span ng-repeat="paragraphJob in notebookJob.paragraphs">
<a style="text-decoration: none !important;"
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
<i style="color: green" class="fa fa-circle-o"
tooltip-placement="top-left"
uib-tooltip="{{paragraphJob.name}} is READY">
</i>
</a>
<a ng-switch-when="FINISHED"
style="text-decoration: none !important;"
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
<i style="color: green" class="fa fa-circle"
tooltip-placement="top-left"
uib-tooltip="{{paragraphJob.name}} is FINISHED">
</i>
</a>
<a ng-switch-when="ABORT"
style="text-decoration: none !important;"
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
<i style="color: orange" class="fa fa-circle"
tooltip-placement="top-left"
uib-tooltip="{{paragraphJob.name}} is ABORT">
</i>
</a>
<a ng-switch-when="ERROR"
style="text-decoration: none !important;"
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
<i style="color: red" class="fa fa-circle"
tooltip-placement="top-left"
uib-tooltip="{{paragraphJob.name}} is ERROR">
</i>
</a>
<a ng-switch-when="PENDING"
style="text-decoration: none !important;"
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
<i style="color: gray" class="fa fa-circle"
tooltip-placement="top-left"
uib-tooltip="{{paragraphJob.name}} is PENDING">
</i>
</a>
<a ng-switch-when="RUNNING"
style="text-decoration: none !important;"
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
<i style="color: blue" class="fa fa-spinner spinAnimation"
tooltip-placement="top-left"
uib-tooltip="{{paragraphJob.name}} is RUNNING">
</i>
</a>
<a ng-switch-default class="icon-question"
style="text-decoration: none !important;"
ng-href="#/notebook/{{notebookJob.noteId}}?paragraph={{paragraphJob.id}}">
<i class="icon-question"
<i ng-style="{'color': $parent.getJobColorByStatus(paragraphJob.status)}"
ng-class="$parent.getJobIconByStatus(paragraphJob.status)"
tooltip-placement="top-left"
uib-tooltip="{{paragraphJob.name}} is {{paragraphJob.status}}">
</i>

View file

@ -174,12 +174,19 @@
}
.paragraph .control {
display: inline-block;
position: absolute;
float: right;
background: rgba(255,255,255,0.85);
color: #999;
margin-top: 1px;
margin-right: 5px;
position: absolute;
clear: both;
right: 15px;
top: 16px;
text-align: right;
font-size: 12px;
margin-top: 5px;
padding: 4px;
}
.paragraph .control li {
@ -268,11 +275,8 @@ table.table-shortcut {
*/
.paragraph .title {
display: inline-block;
float: left;
margin: 0;
font-size: 12px;
margin-bottom: 7px;
min-height: 24px;
}
.paragraph .title div {
@ -560,3 +564,7 @@ table.table-striped {
.markdown-body h4 {
font-size: 16px;
}
.network-labels {
margin: 0.2em;
}

View file

@ -15,29 +15,24 @@ limitations under the License.
<div id="{{paragraph.id}}_container"
ng-class="{'paragraph': !asIframe, 'paragraphAsIframe': asIframe}">
<div>
<div ng-if="paragraph.config.title"
id="{{paragraph.id}}_title"
ng-controller="ElasticInputCtrl as input"
class="title">
<input type="text"
pu-elastic-input
style="min-width: 400px; max-width: 80%;"
placeholder="Untitled"
ng-model="paragraph.title"
ng-if="input.showEditor"
ng-escape="input.showEditor = false; paragraph.title = oldTitle;"
ng-blur="setTitle(paragraph); input.showEditor = false"
ng-enter="setTitle(paragraph); input.showEditor = false"
focus-if="input.showEditor" />
<div ng-click="input.showEditor = !asIframe && !viewOnly && !revisionView; oldTitle = paragraph.title;"
ng-show="!input.showEditor"
ng-bind-html="paragraph.title || 'Untitled'">
</div>
<div ng-if="paragraph.config.title"
id="{{paragraph.id}}_title"
ng-controller="ElasticInputCtrl as input"
class="title">
<input type="text"
pu-elastic-input
style="min-width: 400px; max-width: 80%;"
placeholder="Untitled"
ng-model="paragraph.title"
ng-if="input.showEditor"
ng-escape="input.showEditor = false; paragraph.title = oldTitle;"
ng-blur="setTitle(paragraph); input.showEditor = false"
ng-enter="setTitle(paragraph); input.showEditor = false"
focus-if="input.showEditor" />
<div ng-click="input.showEditor = !asIframe && !viewOnly && !revisionView; oldTitle = paragraph.title;"
ng-show="!input.showEditor"
ng-bind-html="paragraph.title || 'Untitled'">
</div>
<div ng-include src="'app/notebook/paragraph/paragraph-control.html'"></div>
<div style="display: inline-block; clear: both;"></div>
</div>
<div>
@ -68,6 +63,8 @@ limitations under the License.
</div>
</div>
<div ng-include src="'app/notebook/paragraph/paragraph-control.html'"></div>
<div ng-if="!asIframe" class="paragraphFooter">
<div ng-show="!paragraph.config.tableHide && !viewOnly"
id="{{paragraph.id}}_executionTime"

View file

@ -13,12 +13,13 @@ limitations under the License.
-->
<div id="{{id}}_switch"
ng-if="(type == 'TABLE' || apps.length > 0 || suggestion.available && suggestion.available.length > 0) && !asIframe && !viewOnly"
ng-if="(type == 'TABLE' || type == 'NETWORK' || apps.length > 0 || suggestion.available && suggestion.available.length > 0) && !asIframe && !viewOnly"
class="result-chart-selector">
<div ng-if="type == 'TABLE'" class="btn-group">
<div ng-if="type == 'TABLE' || type == 'NETWORK'" class="btn-group">
<button type="button" class="btn btn-default btn-sm"
ng-repeat="viz in builtInTableDataVisualizationList track by $index"
ng-if="viz.supports.indexOf(type) > -1"
ng-class="{'active' : viz.id == graphMode && !config.helium.activeApp}"
ng-click="switchViz(viz.id)"
tooltip-placement="bottom" uib-tooltip="{{viz.name ? viz.name : ''}}"
@ -28,7 +29,7 @@ limitations under the License.
<div class="btn-group">
<button type="button"
ng-if="type != 'TABLE'"
ng-if="type != 'TABLE' && type != 'NETWORK'"
ng-click="switchApp()"
ng-class="{'active' : !config.helium.activeApp}"
class="btn btn-default btn-sm"><i class="fa fa-terminal"></i>
@ -73,7 +74,7 @@ limitations under the License.
</div>
<div class="btn-group"
ng-if="type == 'TABLE' && !asIframe && !viewOnly"
ng-if="(type == 'TABLE' || type == 'NETWORK') && !asIframe && !viewOnly"
style="margin-bottom: 10px;">
<button type="button" class="btn btn-default btn-sm"
style="margin-left:10px"
@ -93,7 +94,7 @@ limitations under the License.
</div>
<span
ng-if="type=='TABLE' && !config.helium.activeApp && !asIframe && !viewOnly"
ng-if="(type == 'TABLE' || type == 'NETWORK') && !config.helium.activeApp && graphMode!='table' && !asIframe && !viewOnly"
style="margin-left:10px; cursor:pointer; display: inline-block; vertical-align:top; position: relative; line-height:30px;">
<a class="btnText" ng-click="toggleGraphSetting()">
settings <span ng-class="config.graph.optionOpen ? 'fa fa-caret-up' : 'fa fa-caret-down'"></span>

View file

@ -14,13 +14,14 @@
import moment from 'moment'
import TableData from '../../../tabledata/tabledata'
import DatasetFactory from '../../../tabledata/datasetfactory'
import TableVisualization from '../../../visualization/builtins/visualization-table'
import BarchartVisualization from '../../../visualization/builtins/visualization-barchart'
import PiechartVisualization from '../../../visualization/builtins/visualization-piechart'
import AreachartVisualization from '../../../visualization/builtins/visualization-areachart'
import LinechartVisualization from '../../../visualization/builtins/visualization-linechart'
import ScatterchartVisualization from '../../../visualization/builtins/visualization-scatterchart'
import NetworkVisualization from '../../../visualization/builtins/visualization-d3network'
import {
DefaultDisplayType,
SpellResult,
@ -44,36 +45,48 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio
{
id: 'table', // paragraph.config.graph.mode
name: 'Table', // human readable name. tooltip
icon: '<i class="fa fa-table"></i>'
icon: '<i class="fa fa-table"></i>',
supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK]
},
{
id: 'multiBarChart',
name: 'Bar Chart',
icon: '<i class="fa fa-bar-chart"></i>',
transformation: 'pivot'
transformation: 'pivot',
supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK]
},
{
id: 'pieChart',
name: 'Pie Chart',
icon: '<i class="fa fa-pie-chart"></i>',
transformation: 'pivot'
transformation: 'pivot',
supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK]
},
{
id: 'stackedAreaChart',
name: 'Area Chart',
icon: '<i class="fa fa-area-chart"></i>',
transformation: 'pivot'
transformation: 'pivot',
supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK]
},
{
id: 'lineChart',
name: 'Line Chart',
icon: '<i class="fa fa-line-chart"></i>',
transformation: 'pivot'
transformation: 'pivot',
supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK]
},
{
id: 'scatterChart',
name: 'Scatter Chart',
icon: '<i class="cf cf-scatter-chart"></i>'
icon: '<i class="cf cf-scatter-chart"></i>',
supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK]
},
{
id: 'network',
name: 'Network',
icon: '<i class="fa fa-share-alt"></i>',
supports: [DefaultDisplayType.NETWORK]
}
]
@ -104,6 +117,10 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio
'scatterChart': {
class: ScatterchartVisualization,
instance: undefined
},
'network': {
class: NetworkVisualization,
instance: undefined
}
}
@ -159,7 +176,8 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio
$scope.builtInTableDataVisualizationList.push({
id: vis.id,
name: vis.name,
icon: $sce.trustAsHtml(vis.icon)
icon: $sce.trustAsHtml(vis.icon),
supports: [DefaultDisplayType.TABLE, DefaultDisplayType.NETWORK]
})
builtInVisualizations[vis.id] = {
class: vis.class
@ -253,18 +271,23 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio
// enable only when it is last result
enableHelium = (index === paragraphRef.results.msg.length - 1)
if ($scope.type === 'TABLE') {
tableData = new TableData()
if ($scope.type === 'TABLE' || $scope.type === 'NETWORK') {
tableData = new DatasetFactory().createDataset($scope.type)
tableData.loadParagraphResult({type: $scope.type, msg: data})
$scope.tableDataColumns = tableData.columns
$scope.tableDataComment = tableData.comment
if ($scope.type === 'NETWORK') {
$scope.networkNodes = tableData.networkNodes
$scope.networkRelationships = tableData.networkRelationships
$scope.networkProperties = tableData.networkProperties
}
} else if ($scope.type === 'IMG') {
$scope.imageData = data
}
}
$scope.createDisplayDOMId = function (baseDOMId, type) {
if (type === DefaultDisplayType.TABLE) {
if (type === DefaultDisplayType.TABLE || type === DefaultDisplayType.NETWORK) {
return `${baseDOMId}_graph`
} else if (type === DefaultDisplayType.HTML) {
return `${baseDOMId}_html`
@ -281,7 +304,7 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio
$scope.renderDefaultDisplay = function (targetElemId, type, data, refresh) {
const afterLoaded = () => {
if (type === DefaultDisplayType.TABLE) {
if (type === DefaultDisplayType.TABLE || type === DefaultDisplayType.NETWORK) {
renderGraph(targetElemId, $scope.graphMode, refresh)
} else if (type === DefaultDisplayType.HTML) {
renderHtml(targetElemId, data)

View file

@ -57,3 +57,59 @@
font-weight: 400;
text-align: center;
}
/* D3 Graph Configuration */
marker {
fill: #D3D3D3;
}
path.link {
fill: none;
stroke-width: 3px;
stroke: #D3D3D3;
}
path.textpath {
fill: none;
stroke: none;
}
text {
font-size: 12px;
pointer-events: none;
}
text.shadow {
stroke: #fff;
stroke-width: 3px;
stroke-opacity: .8;
}
text.nodeLabel {
font-size: 1em;
pointer-events: none;
}
/* D3 Graph Configuration */
marker {
fill: #D3D3D3;
}
path.link {
fill: none;
stroke-width: 3px;
stroke: #D3D3D3;
}
path.textpath {
fill: none;
stroke: none;
}
text {
font-size: 12px;
pointer-events: none;
}
text.shadow {
stroke: #fff;
stroke-width: 3px;
stroke-opacity: .8;
}
text.nodeLabel {
font-size: 1em;
pointer-events: none;
}

View file

@ -23,7 +23,7 @@ limitations under the License.
resize='{"allowresize": "{{!asIframe && !viewOnly}}", "graphType": "{{type}}"}'
resizable on-resize="resize(width, height);">
<div ng-if="type=='TABLE'"
<div ng-if="type=='TABLE' || type == 'NETWORK'"
ng-style="getPointerEvent()">
<!-- setting -->
<div class="option lightBold" style="overflow: visible;"
@ -36,6 +36,26 @@ limitations under the License.
ng-show="graphMode == viz.id"></div>
</div>
<div id="p{{id}}_network_header"
ng-if="type == 'NETWORK' && graphMode == 'network' && networkNodes != null">
<ul class="list-inline">
<li>Nodes <span class="badge">{{networkNodes.count}}</span>:</li>
<li ng-repeat="(labelName, labelColor) in networkNodes.labels" style="padding: 0">
<span style="background-color: {{labelColor}} !important;" class="label label-default network-badge">
{{labelName}}
</span>
</li>
</ul>
<ul class="list-inline">
<li ng-if="networkRelationships != null">Relationships <span class="badge">{{networkRelationships.count}}</span>:</li>
<li ng-repeat="type in networkRelationships.types" style="padding: 0">
<span class="label label-default network-badge">
{{type}}
</span>
</li>
</ul>
</div>
<!-- graph -->
<div id="p{{id}}_graph"
class="graphContainer"
@ -46,6 +66,12 @@ limitations under the License.
</div>
</div>
<div id="p{{id}}_network_footer"
ng-if="type == 'NETWORK' && graphMode == 'network'">
<ul class="list-inline">
</ul>
</div>
<div id="{{id}}_comment"
class="text"
ng-if="tableDataComment"

View file

@ -21,6 +21,7 @@ export const DefaultDisplayType = {
HTML: 'HTML',
ANGULAR: 'ANGULAR',
TEXT: 'TEXT',
NETWORK: 'NETWORK'
}
export const DefaultDisplayMagic = {
@ -29,6 +30,7 @@ export const DefaultDisplayMagic = {
'%html': DefaultDisplayType.HTML,
'%angular': DefaultDisplayType.ANGULAR,
'%text': DefaultDisplayType.TEXT,
'%network': DefaultDisplayType.NETWORK,
}
export class DataWithType {

View file

@ -0,0 +1,36 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* The abstract dataset rapresentation
*/
class Dataset {
/**
* Load the paragraph result, every Dataset implementation must override this method
* where is contained the business rules to convert the paragraphResult object to the related dataset type
*/
loadParagraphResult(paragraphResult) {
// override this
}
}
/**
* Dataset types
*/
const DatasetType = Object.freeze({
NETWORK: 'NETWORK',
TABLE: 'TABLE'
})
export {Dataset, DatasetType}

View file

@ -0,0 +1,33 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import TableData from './tabledata'
import NetworkData from './networkdata'
import {DatasetType} from './dataset'
/**
* Create table data object from paragraph table type result
*/
export default class DatasetFactory {
createDataset(datasetType) {
switch (datasetType) {
case DatasetType.NETWORK:
return new NetworkData()
case DatasetType.TABLE:
return new TableData()
default:
throw new Error('Dataset type not found')
}
}
}

View file

@ -0,0 +1,46 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import NetworkData from './networkdata.js'
import TableData from './tabledata.js'
import {DatasetType} from './dataset.js'
import DatasetFactory from './datasetfactory.js'
describe('DatasetFactory build', function() {
let df
beforeAll(function() {
df = new DatasetFactory()
})
it('should create a TableData instance', function() {
let td = df.createDataset(DatasetType.TABLE)
expect(td instanceof TableData).toBeTruthy()
expect(td.columns.length).toBe(0)
expect(td.rows.length).toBe(0)
})
it('should create a NetworkData instance', function() {
let nd = df.createDataset(DatasetType.NETWORK)
expect(nd instanceof NetworkData).toBeTruthy()
expect(nd.columns.length).toBe(0)
expect(nd.rows.length).toBe(0)
expect(nd.graph).toEqual({})
})
it('should thrown an Error', function() {
expect(function() { df.createDataset('text') })
.toThrow(new Error('Dataset type not found'))
})
})

View file

@ -0,0 +1,48 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Transformation from './transformation'
/**
* trasformation settings for network visualization
*/
export default class NetworkTransformation extends Transformation {
getSetting() {
let self = this
let configObj = self.config
return {
template: 'app/tabledata/network_settings.html',
scope: {
config: configObj,
isEmptyObject: function(obj) {
obj = obj || {}
return angular.equals(obj, {})
},
setNetworkLabel: function(label, value) {
configObj.properties[label].selected = value
},
saveConfig: function() {
self.emitConfig(configObj)
}
}
}
}
setConfig(config) {
}
transform(networkData) {
return networkData
}
}

View file

@ -0,0 +1,74 @@
<!--
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="row">
<form>
<fieldset class=" col-xs-12">
<h4>Force Layout settings</h4>
<div class="form-check col-xs-4">
<label for="{{$id}}_timeout">Stop Force layout after</label>
<div class="input-group">
<input type="text" class="form-control" ng-model="config.d3Graph.forceLayout.timeout" id="{{$id}}_timeout" />
<span class="input-group-addon">ms</span>
</div>
</div>
<div class="form-check col-xs-4">
<div class="form-group">
<label for="{{$id}}_charge">Force Layout Charge</label>
<input type="text" class="form-control" ng-model="config.d3Graph.forceLayout.charge" id="{{$id}}_charge" />
</div>
</div>
<div class="form-check col-xs-4">
<div class="form-group">
<label for="{{$id}}_linkDistance">Force Layout Link Distance</label>
<input type="text" class="form-control" ng-model="config.d3Graph.forceLayout.linkDistance" id="{{$id}}_linkDistance" />
</div>
</div>
</fieldset>
<fieldset class=" col-xs-12">
<h4>Globals</h4>
<div class="form-check col-xs-4">
<div class="form-group">
<label for="{{$id}}_charge">Minimum scale to show node and edge labels</label>
<input type="text" class="form-control" ng-model="config.d3Graph.zoom.minScale" id="{{$id}}_minScale" />
</div>
</div>
</fieldset>
<fieldset class="form-group col-xs-12">
<h4>Choose node labels</h4>
<div ng-if="isEmptyObject(config.properties)">
No labels to set
</div>
<div class="btn-group network-labels network-badge-settings"
ng-repeat="(key, value) in config.properties track by key">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{key}}:<i>{{config.properties[key].selected}}</i> <div class="caret"></div>
</button>
<ul class="dropdown-menu">
<li ng-repeat="val in value.keys" ng-click="setNetworkLabel(key, val)">
<a><i ng-if="config.properties[key].selected == val" class="glyphicon glyphicon-ok">
</i> {{val}}</a>
</li>
</ul>
</div>
</fieldset>
<fieldset class="form-group col-xs-12">
<button type="submit" class="btn btn-primary btn-sm" ng-click="saveConfig()">
<span class="glyphicon glyphicon-floppy-disk"></span>
Save configuration
</button>
</fieldset>
</form>
</div>

View file

@ -0,0 +1,145 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import TableData from './tabledata'
import {DatasetType} from './dataset'
/**
* Create network data object from paragraph graph type result
*/
export default class NetworkData extends TableData {
constructor(graph) {
super()
this.graph = graph || {}
if (this.graph.nodes) {
this.loadParagraphResult({msg: JSON.stringify(graph), type: DatasetType.NETWORK})
}
}
loadParagraphResult(paragraphResult) {
if (!paragraphResult || paragraphResult.type !== DatasetType.NETWORK) {
console.log('Can not load paragraph result')
return
}
this.graph = JSON.parse(paragraphResult.msg.trim() || '{}')
if (!this.graph.nodes) {
console.log('Graph result is empty')
return
}
this.setNodesDefaults()
this.setEdgesDefaults()
this.networkNodes = angular.equals({}, this.graph.labels || {})
? null : {count: this.graph.nodes.length, labels: this.graph.labels}
this.networkRelationships = angular.equals([], this.graph.types || [])
? null : {count: this.graph.edges.length, types: this.graph.types}
let rows = []
let comment = ''
let entities = this.graph.nodes.concat(this.graph.edges)
let baseColumnNames = [{name: 'id', index: 0, aggr: 'sum'},
{name: 'label', index: 1, aggr: 'sum'}]
let internalFieldsToJump = ['count', 'size', 'totalCount',
'data', 'x', 'y', 'labels']
let baseCols = _.map(baseColumnNames, function(col) { return col.name })
let keys = _.map(entities, function(elem) { return Object.keys(elem.data || {}) })
keys = _.flatten(keys)
keys = _.uniq(keys).filter(function(key) {
return baseCols.indexOf(key) === -1
})
let columnNames = baseColumnNames.concat(_.map(keys, function(elem, i) {
return {name: elem, index: i + baseColumnNames.length, aggr: 'sum'}
}))
for (let i = 0; i < entities.length; i++) {
let entity = entities[i]
let col = []
let col2 = []
entity.data = entity.data || {}
for (let j = 0; j < columnNames.length; j++) {
let name = columnNames[j].name
let value = name in entity && internalFieldsToJump.indexOf(name) === -1
? entity[name] : entity.data[name]
let parsedValue = value === null || value === undefined ? '' : value
col.push(parsedValue)
col2.push({key: name, value: parsedValue})
}
rows.push(col)
}
this.comment = comment
this.columns = columnNames
this.rows = rows
}
setNodesDefaults() {
}
setEdgesDefaults() {
this.graph.edges
.sort((a, b) => {
if (a.source > b.source) {
return 1
} else if (a.source < b.source) {
return -1
} else if (a.target > b.target) {
return 1
} else if (a.target < b.target) {
return -1
} else {
return 0
}
})
this.graph.edges
.forEach((edge, index) => {
let prevEdge = this.graph.edges[index - 1]
edge.count = (index > 0 && +edge.source === +prevEdge.source && +edge.target === +prevEdge.target
? prevEdge.count : 0) + 1
edge.totalCount = this.graph.edges
.filter((innerEdge) => +edge.source === +innerEdge.source && +edge.target === +innerEdge.target)
.length
})
this.graph.edges
.forEach((edge) => {
if (typeof +edge.source === 'number') {
edge.source = this.graph.nodes.filter((node) => +edge.source === +node.id)[0] || null
}
if (typeof +edge.target === 'number') {
edge.target = this.graph.nodes.filter((node) => +edge.target === +node.id)[0] || null
}
})
}
getNetworkProperties() {
let baseCols = ['id', 'label']
let properties = {}
this.graph.nodes.forEach(function(node) {
let hasLabel = 'label' in node && node.label !== ''
if (!hasLabel) {
return
}
let label = node.label
let hasKey = hasLabel && label in properties
let keys = _.uniq(Object.keys(node.data || {})
.concat(hasKey ? properties[label].keys : baseCols))
if (!hasKey) {
properties[label] = {selected: 'label'}
}
properties[label].keys = keys
})
return properties
}
}

View file

@ -0,0 +1,46 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import NetworkData from './networkdata.js'
import {DatasetType} from './dataset.js'
describe('NetworkData build', function() {
let nd
beforeEach(function() {
nd = new NetworkData()
})
it('should initialize the default value', function() {
expect(nd.columns.length).toBe(0)
expect(nd.rows.length).toBe(0)
expect(nd.graph).toEqual({})
})
it('should able to create NetowkData from paragraph result', function() {
let jsonExpected = {nodes: [{id: 1}, {id: 2}], edges: [{source: 2, target: 1, id: 1}]}
nd.loadParagraphResult({
type: DatasetType.NETWORK,
msg: JSON.stringify(jsonExpected)
})
expect(nd.columns.length).toBe(2)
expect(nd.rows.length).toBe(3)
expect(nd.graph.nodes[0].id).toBe(jsonExpected.nodes[0].id)
expect(nd.graph.nodes[1].id).toBe(jsonExpected.nodes[1].id)
expect(nd.graph.edges[0].id).toBe(jsonExpected.edges[0].id)
expect(nd.graph.edges[0].source.id).toBe(jsonExpected.nodes[1].id)
expect(nd.graph.edges[0].target.id).toBe(jsonExpected.nodes[0].id)
})
})

View file

@ -11,19 +11,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Dataset, DatasetType} from './dataset'
/**
* Create table data object from paragraph table type result
*/
export default class TableData {
export default class TableData extends Dataset {
constructor (columns, rows, comment) {
super()
this.columns = columns || []
this.rows = rows || []
this.comment = comment || ''
}
loadParagraphResult (paragraphResult) {
if (!paragraphResult || paragraphResult.type !== 'TABLE') {
if (!paragraphResult || paragraphResult.type !== DatasetType.TABLE) {
console.log('Can not load paragraph result')
return
}

View file

@ -0,0 +1,263 @@
/*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Visualization from '../visualization'
import NetworkTransformation from '../../tabledata/network'
/**
* Visualize data in network format
*/
export default class NetworkVisualization extends Visualization {
constructor(targetEl, config) {
super(targetEl, config)
console.log('Init network viz')
if (!config.properties) {
config.properties = {}
}
if (!config.d3Graph) {
config.d3Graph = {
forceLayout: {
timeout: 10000,
charge: -120,
linkDistance: 80,
},
zoom: {
minScale: 1.3
}
}
}
this.targetEl.addClass('network')
this.containerId = this.targetEl.prop('id')
this.force = null
this.svg = null
this.$timeout = angular.injector(['ng']).get('$timeout')
this.$interpolate = angular.injector(['ng']).get('$interpolate')
this.transformation = new NetworkTransformation(config)
}
refresh() {
console.log('refresh')
}
render(networkData) {
if (!('graph' in networkData)) {
console.log('graph not found')
return
}
console.log('Render Graph Visualization')
let transformationConfig = this.transformation.getSetting().scope.config
console.log('cfg', transformationConfig)
if (transformationConfig && angular.equals({}, transformationConfig.properties)) {
transformationConfig.properties = networkData.getNetworkProperties()
}
this.targetEl.empty().append('<svg></svg>')
let width = this.targetEl.width()
let height = this.targetEl.height()
let self = this
let defaultOpacity = 0
let nodeSize = 10
let textOffset = 3
let linkSize = 10
let arcPath = (leftHand, d) => {
let start = leftHand ? d.source : d.target
let end = leftHand ? d.target : d.source
let dx = end.x - start.x
let dy = end.y - start.y
let dr = d.totalCount === 1
? 0 : Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) / (1 + (1 / d.totalCount) * (d.count - 1))
let sweep = leftHand ? 0 : 1
return `M${start.x},${start.y}A${dr},${dr} 0 0,${sweep} ${end.x},${end.y}`
}
// Use elliptical arc path segments to doubly-encode directionality.
let tick = () => {
// Links
linkPath.attr('d', function(d) {
return arcPath(true, d)
})
textPath.attr('d', function(d) {
return arcPath(d.source.x < d.target.x, d)
})
// Nodes
circle.attr('transform', (d) => `translate(${d.x},${d.y})`)
text.attr('transform', (d) => `translate(${d.x},${d.y})`)
}
let setOpacity = (scale) => {
let opacity = scale >= +transformationConfig.d3Graph.zoom.minScale ? 1 : 0
this.svg.selectAll('.nodeLabel')
.style('opacity', opacity)
this.svg.selectAll('textPath')
.style('opacity', opacity)
}
let zoom = d3.behavior.zoom()
.scaleExtent([1, 10])
.on('zoom', () => {
console.log('zoom')
setOpacity(d3.event.scale)
container.attr('transform', `translate(${d3.event.translate})scale(${d3.event.scale})`)
})
this.svg = d3.select(`#${this.containerId} svg`)
.attr('width', width)
.attr('height', height)
.call(zoom)
this.force = d3.layout.force()
.charge(transformationConfig.d3Graph.forceLayout.charge)
.linkDistance(transformationConfig.d3Graph.forceLayout.linkDistance)
.on('tick', tick)
.nodes(networkData.graph.nodes)
.links(networkData.graph.edges)
.size([width, height])
.on('start', () => {
console.log('force layout start')
this.$timeout(() => { this.force.stop() }, transformationConfig.d3Graph.forceLayout.timeout)
})
.on('end', () => {
console.log('force layout stop')
setOpacity(zoom.scale())
})
.start()
let renderFooterOnClick = (entity, type) => {
let footerId = this.containerId + '_footer'
let obj = {id: entity.id, label: entity.defaultLabel || entity.label, type: type}
let html = [this.$interpolate(['<li><b>{{type}}_id:</b>&nbsp{{id}}</li>',
'<li><b>{{type}}_type:</b>&nbsp{{label}}</li>'].join(''))(obj)]
html = html.concat(_.map(entity.data, (v, k) => {
return this.$interpolate('<li><b>{{field}}:</b>&nbsp{{value}}</li>')({field: k, value: v})
}))
angular.element('#' + footerId)
.find('.list-inline')
.empty()
.append(html.join(''))
}
let drag = d3.behavior.drag()
.origin((d) => d)
.on('dragstart', function(d) {
console.log('dragstart')
d3.event.sourceEvent.stopPropagation()
d3.select(this).classed('dragging', true)
self.force.stop()
})
.on('drag', function(d) {
console.log('drag')
d.px += d3.event.dx
d.py += d3.event.dy
d.x += d3.event.dx
d.y += d3.event.dy
})
.on('dragend', function(d) {
console.log('dragend')
d.fixed = true
d3.select(this).classed('dragging', false)
self.force.resume()
})
let container = this.svg.append('g')
if (networkData.graph.directed) {
container.append('svg:defs').selectAll('marker')
.data(['arrowMarker-' + this.containerId])
.enter()
.append('svg:marker')
.attr('id', String)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 16)
.attr('refY', 0)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
}
// Links
let link = container.append('svg:g')
.on('click', () => {
renderFooterOnClick(d3.select(d3.event.target).datum(), 'edge')
})
.selectAll('g.link')
.data(self.force.links())
.enter()
.append('g')
let getPathId = (d) => this.containerId + '_' + d.source.index + '_' + d.target.index + '_' + d.count
let showLabel = (d) => this._showNodeLabel(d)
let linkPath = link.append('svg:path')
.attr('class', 'link')
.attr('size', linkSize)
.attr('marker-end', `url(#arrowMarker-${this.containerId})`)
let textPath = link.append('svg:path')
.attr('id', getPathId)
.attr('class', 'textpath')
container.append('svg:g')
.selectAll('.pathLabel')
.data(self.force.links())
.enter()
.append('svg:text')
.attr('class', 'pathLabel')
.append('svg:textPath')
.attr('startOffset', '50%')
.attr('text-anchor', 'middle')
.attr('xlink:href', (d) => '#' + getPathId(d))
.text((d) => d.label)
.style('opacity', defaultOpacity)
// Nodes
let circle = container.append('svg:g')
.on('click', () => {
renderFooterOnClick(d3.select(d3.event.target).datum(), 'node')
})
.selectAll('circle')
.data(self.force.nodes())
.enter().append('svg:circle')
.attr('r', (d) => nodeSize)
.attr('fill', (d) => networkData.graph.labels && d.label in networkData.graph.labels
? networkData.graph.labels[d.label] : '#000000')
.call(drag)
let text = container.append('svg:g').selectAll('g')
.data(self.force.nodes())
.enter().append('svg:g')
text.append('svg:text')
.attr('x', (d) => nodeSize + textOffset)
.attr('size', nodeSize)
.attr('y', '.31em')
.attr('class', (d) => 'nodeLabel shadow label-' + d.label)
.text(showLabel)
.style('opacity', defaultOpacity)
text.append('svg:text')
.attr('x', (d) => nodeSize + textOffset)
.attr('size', nodeSize)
.attr('y', '.31em')
.attr('class', (d) => 'nodeLabel label-' + d.label)
.text(showLabel)
.style('opacity', defaultOpacity)
}
destroy() {
}
_showNodeLabel(d) {
let transformationConfig = this.transformation.getSetting().scope.config
let selectedLabel = (transformationConfig.properties[d.label] || {selected: 'label'}).selected
return d.data[selectedLabel] || d[selectedLabel]
}
getTransformation() {
return this.transformation
}
}

View file

@ -86,6 +86,7 @@ export default class TableVisualization extends Visualization {
columnDefs: columnNames.map(colName => {
return {
displayName: colName,
name: colName,
type: DefaultTableColumnType,
cellTemplate: `

View file

@ -38,7 +38,7 @@ function NavCtrl ($scope, $rootScope, $http, $routeParams, $location,
function getZeppelinVersion () {
$http.get(baseUrlSrv.getRestApiBase() + '/version').success(
function (data, status, headers, config) {
$rootScope.zeppelinVersion = data.body
$rootScope.zeppelinVersion = data.body.version
}).error(
function (data, status, headers, config) {
console.log('Error %o %o', status, data.message)

View file

@ -35,7 +35,7 @@ function resizable () {
let colStep = window.innerWidth / 12
elem.off('resizestop')
let conf = angular.copy(resizableConfig)
if (resize.graphType === 'TABLE' || resize.graphType === 'TEXT') {
if (resize.graphType === 'TABLE' || resize.graphType === 'NETWORK' || resize.graphType === 'TEXT') {
conf.grid = [colStep, 10]
conf.minHeight = 100
} else {

View file

@ -424,8 +424,16 @@ public class ZeppelinConfiguration extends XMLConfiguration {
return getRelativeDir(ConfVars.ZEPPELIN_HELIUM_REGISTRY);
}
public String getHeliumNpmRegistry() {
return getString(ConfVars.ZEPPELIN_HELIUM_NPM_REGISTRY);
public String getHeliumNodeInstallerUrl() {
return getString(ConfVars.ZEPPELIN_HELIUM_NODE_INSTALLER_URL);
}
public String getHeliumNpmInstallerUrl() {
return getString(ConfVars.ZEPPELIN_HELIUM_NPM_INSTALLER_URL);
}
public String getHeliumYarnInstallerUrl() {
return getString(ConfVars.ZEPPELIN_HELIUM_YARNPKG_INSTALLER_URL);
}
public String getNotebookAuthorizationPath() {
@ -643,7 +651,12 @@ public class ZeppelinConfiguration extends XMLConfiguration {
ZEPPELIN_CONF_DIR("zeppelin.conf.dir", "conf"),
ZEPPELIN_DEP_LOCALREPO("zeppelin.dep.localrepo", "local-repo"),
ZEPPELIN_HELIUM_REGISTRY("zeppelin.helium.registry", "helium," + HELIUM_PACKAGE_DEFAULT_URL),
ZEPPELIN_HELIUM_NPM_REGISTRY("zeppelin.helium.npm.registry", "http://registry.npmjs.org/"),
ZEPPELIN_HELIUM_NODE_INSTALLER_URL("zeppelin.helium.node.installer.url",
"https://nodejs.org/dist/"),
ZEPPELIN_HELIUM_NPM_INSTALLER_URL("zeppelin.helium.npm.installer.url",
"http://registry.npmjs.org/"),
ZEPPELIN_HELIUM_YARNPKG_INSTALLER_URL("zeppelin.helium.yarnpkg.installer.url",
"https://github.com/yarnpkg/yarn/releases/download/"),
// Allows a way to specify a ',' separated list of allowed origins for rest and websockets
// i.e. http://localhost:8080
ZEPPELIN_ALLOWED_ORIGINS("zeppelin.server.allowed.origins", "*"),

View file

@ -48,24 +48,24 @@ import org.apache.zeppelin.conf.ZeppelinConfiguration;
* Load helium visualization & spell
*/
public class HeliumBundleFactory {
Logger logger = LoggerFactory.getLogger(HeliumBundleFactory.class);
private final String NODE_VERSION = "v6.9.1";
private final String NPM_VERSION = "3.10.8";
private final String YARN_VERSION = "v0.21.3";
private Logger logger = LoggerFactory.getLogger(HeliumBundleFactory.class);
private static final String NODE_VERSION = "v6.9.1";
private static final String NPM_VERSION = "3.10.8";
private static final String YARN_VERSION = "v0.21.3";
private static final String NPM_PACKAGE_NAME = "npm";
public static final String HELIUM_LOCAL_REPO = "helium-bundle";
public static final String HELIUM_BUNDLES_DIR = "bundles";
public static final String HELIUM_LOCAL_MODULE_DIR = "local_modules";
public static final String HELIUM_BUNDLES_SRC_DIR = "src";
public static final String HELIUM_BUNDLES_SRC = "load.js";
public static final String YARN_CACHE_DIR = "yarn-cache";
public static final String PACKAGE_JSON = "package.json";
public static final String HELIUM_BUNDLE_CACHE = "helium.bundle.cache.js";
public static final String HELIUM_BUNDLE = "helium.bundle.js";
public static final String HELIUM_BUNDLES_VAR = "heliumBundles";
private final int FETCH_RETRY_COUNT = 2;
private final int FETCH_RETRY_FACTOR_COUNT = 1;
private final int FETCH_RETRY_MIN_TIMEOUT = 5000; // Milliseconds
protected static final String HELIUM_LOCAL_REPO = "helium-bundle";
private static final String HELIUM_BUNDLES_DIR = "bundles";
private static final String HELIUM_LOCAL_MODULE_DIR = "local_modules";
private static final String HELIUM_BUNDLES_SRC_DIR = "src";
private static final String HELIUM_BUNDLES_SRC = "load.js";
private static final String YARN_CACHE_DIR = "yarn-cache";
private static final String PACKAGE_JSON = "package.json";
private static final String HELIUM_BUNDLE_CACHE = "helium.bundle.cache.js";
private static final String HELIUM_BUNDLE = "helium.bundle.js";
private static final String HELIUM_BUNDLES_VAR = "heliumBundles";
private static final int FETCH_RETRY_COUNT = 2;
private static final int FETCH_RETRY_FACTOR_COUNT = 1;
private static final int FETCH_RETRY_MIN_TIMEOUT = 5000; // Milliseconds
private final FrontendPluginFactory frontEndPluginFactory;
private final File nodeInstallationDirectory;
@ -73,17 +73,16 @@ public class HeliumBundleFactory {
private final File heliumBundleDirectory;
private final File heliumLocalModuleDirectory;
private final File yarnCacheDir;
private ZeppelinConfiguration conf;
private File tabledataModulePath;
private File visualizationModulePath;
private File spellModulePath;
private String defaultNodeRegistryUrl;
private String defaultNpmRegistryUrl;
private String defaultYarnRegistryUrl;
private String defaultNodeInstallerUrl;
private String defaultNpmInstallerUrl;
private String defaultYarnInstallerUrl;
private Gson gson;
private boolean nodeAndNpmInstalled = false;
ByteArrayOutputStream out = new ByteArrayOutputStream();
private ByteArrayOutputStream out = new ByteArrayOutputStream();
public HeliumBundleFactory(
ZeppelinConfiguration conf,
@ -98,7 +97,7 @@ public class HeliumBundleFactory {
this.spellModulePath = spellModulePath;
}
public HeliumBundleFactory(
private HeliumBundleFactory(
ZeppelinConfiguration conf,
File nodeInstallationDir,
File moduleDownloadPath) throws TaskRunnerException {
@ -106,11 +105,9 @@ public class HeliumBundleFactory {
this.heliumBundleDirectory = new File(heliumLocalRepoDirectory, HELIUM_BUNDLES_DIR);
this.heliumLocalModuleDirectory = new File(heliumLocalRepoDirectory, HELIUM_LOCAL_MODULE_DIR);
this.yarnCacheDir = new File(heliumLocalRepoDirectory, YARN_CACHE_DIR);
this.conf = conf;
// To be done in ZEPPELIN-2214: Soft-code installer urls
this.defaultNodeRegistryUrl = "https://nodejs.org/dist/";
this.defaultNpmRegistryUrl = conf.getHeliumNpmRegistry();
this.defaultYarnRegistryUrl = "https://github.com/yarnpkg/yarn/releases/download/";
this.defaultNodeInstallerUrl = conf.getHeliumNodeInstallerUrl();
this.defaultNpmInstallerUrl = conf.getHeliumNpmInstallerUrl();
this.defaultYarnInstallerUrl = conf.getHeliumYarnInstallerUrl();
nodeInstallationDirectory = (nodeInstallationDir == null) ?
heliumLocalRepoDirectory : nodeInstallationDir;
@ -127,21 +124,21 @@ public class HeliumBundleFactory {
}
try {
NodeInstaller nodeInstaller = frontEndPluginFactory
.getNodeInstaller(getProxyConfig(isSecure(defaultNodeRegistryUrl)));
.getNodeInstaller(getProxyConfig(isSecure(defaultNodeInstallerUrl)));
nodeInstaller.setNodeVersion(NODE_VERSION);
nodeInstaller.setNodeDownloadRoot(defaultNodeRegistryUrl);
nodeInstaller.setNodeDownloadRoot(defaultNodeInstallerUrl);
nodeInstaller.install();
NPMInstaller npmInstaller = frontEndPluginFactory
.getNPMInstaller(getProxyConfig(isSecure(defaultNpmRegistryUrl)));
.getNPMInstaller(getProxyConfig(isSecure(defaultNpmInstallerUrl)));
npmInstaller.setNpmVersion(NPM_VERSION);
npmInstaller.setNpmDownloadRoot(defaultNpmRegistryUrl + "/" + NPM_PACKAGE_NAME + "/-/");
npmInstaller.setNpmDownloadRoot(defaultNpmInstallerUrl + "/" + NPM_PACKAGE_NAME + "/-/");
npmInstaller.install();
YarnInstaller yarnInstaller = frontEndPluginFactory
.getYarnInstaller(getProxyConfig(isSecure(defaultYarnRegistryUrl)));
.getYarnInstaller(getProxyConfig(isSecure(defaultYarnInstallerUrl)));
yarnInstaller.setYarnVersion(YARN_VERSION);
yarnInstaller.setYarnDownloadRoot(defaultYarnRegistryUrl);
yarnInstaller.setYarnDownloadRoot(defaultYarnInstallerUrl);
yarnInstaller.install();
yarnCacheDir.mkdirs();
String yarnCacheDirPath = yarnCacheDir.getAbsolutePath();
@ -203,48 +200,47 @@ public class HeliumBundleFactory {
buildAllPackages(pkgs, false);
}
public File getHeliumPackageDirectory(String pkgName) {
private File getHeliumPackageDirectory(String pkgName) {
return new File(heliumBundleDirectory, pkgName);
}
public File getHeliumPackageSourceDirectory(String pkgName) {
private File getHeliumPackageSourceDirectory(String pkgName) {
return new File(heliumBundleDirectory, pkgName + "/" + HELIUM_BUNDLES_SRC_DIR);
}
public File getHeliumPackageBundleCache(String pkgName) {
private File getHeliumPackageBundleCache(String pkgName) {
return new File(heliumBundleDirectory, pkgName + "/" + HELIUM_BUNDLE_CACHE);
}
public static List<String> unTgz(File tarFile, File directory) throws IOException {
List<String> result = new ArrayList<String>();
InputStream is = new FileInputStream(tarFile);
GzipCompressorInputStream gcis = new GzipCompressorInputStream(is);
TarArchiveInputStream in = new TarArchiveInputStream(gcis);
TarArchiveEntry entry = in.getNextTarEntry();
while (entry != null) {
if (entry.isDirectory()) {
private static List<String> unTgz(File tarFile, File directory) throws IOException {
List<String> result = new ArrayList<>();
try (TarArchiveInputStream in = new TarArchiveInputStream(
new GzipCompressorInputStream(new FileInputStream(tarFile)))) {
TarArchiveEntry entry = in.getNextTarEntry();
while (entry != null) {
if (entry.isDirectory()) {
entry = in.getNextTarEntry();
continue;
}
File curfile = new File(directory, entry.getName());
File parent = curfile.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
try (OutputStream out = new FileOutputStream(curfile)) {
IOUtils.copy(in, out);
}
result.add(entry.getName());
entry = in.getNextTarEntry();
continue;
}
File curfile = new File(directory, entry.getName());
File parent = curfile.getParentFile();
if (!parent.exists()) {
parent.mkdirs();
}
OutputStream out = new FileOutputStream(curfile);
IOUtils.copy(in, out);
out.close();
result.add(entry.getName());
entry = in.getNextTarEntry();
}
in.close();
return result;
}
/**
* @return main file name of this helium package (relative path)
*/
public String downloadPackage(HeliumPackage pkg, String[] nameAndVersion, File bundleDir,
private String downloadPackage(HeliumPackage pkg, String[] nameAndVersion, File bundleDir,
String templateWebpackConfig, String templatePackageJson,
FrontendPluginFactory fpf) throws IOException, TaskRunnerException {
if (bundleDir.exists()) {
@ -279,7 +275,8 @@ public class HeliumBundleFactory {
npmCommand(fpf, "pack " + pkg.getArtifact());
File extracted = new File(heliumBundleDirectory, "package");
FileUtils.deleteDirectory(extracted);
unTgz(tgz, heliumBundleDirectory);
List<String> entries = unTgz(tgz, heliumBundleDirectory);
for (String entry: entries) logger.debug("Extracted " + entry);
tgz.delete();
FileUtils.copyDirectory(extracted, bundleDir);
FileUtils.deleteDirectory(extracted);
@ -324,7 +321,7 @@ public class HeliumBundleFactory {
return mainFileName;
}
public void prepareSource(HeliumPackage pkg, String[] moduleNameVersion,
private void prepareSource(HeliumPackage pkg, String[] moduleNameVersion,
String mainFileName) throws IOException {
StringBuilder loadJsImport = new StringBuilder();
StringBuilder loadJsRegister = new StringBuilder();
@ -354,7 +351,7 @@ public class HeliumBundleFactory {
loadJsImport.append(loadJsRegister).toString());
}
public synchronized void installNodeModules(FrontendPluginFactory fpf) throws IOException {
private synchronized void installNodeModules(FrontendPluginFactory fpf) throws IOException {
try {
out.reset();
String commandForNpmInstall =
@ -369,7 +366,7 @@ public class HeliumBundleFactory {
}
}
public synchronized File bundleHeliumPackage(FrontendPluginFactory fpf,
private synchronized File bundleHeliumPackage(FrontendPluginFactory fpf,
File bundleDir) throws IOException {
try {
out.reset();
@ -404,10 +401,6 @@ public class HeliumBundleFactory {
}
String[] moduleNameVersion = getNpmModuleNameAndVersion(pkg);
if (moduleNameVersion == null) {
return null;
}
if (moduleNameVersion == null) {
logger.error("Can't get module name and version of package " + pkg.getName());
return null;
@ -466,7 +459,7 @@ public class HeliumBundleFactory {
return bundleCache;
}
public synchronized void buildAllPackages(List<HeliumPackage> pkgs, boolean rebuild)
private synchronized void buildAllPackages(List<HeliumPackage> pkgs, boolean rebuild)
throws IOException {
if (pkgs == null || pkgs.size() == 0) {
@ -485,7 +478,7 @@ public class HeliumBundleFactory {
}
}
void copyFrameworkModule(boolean recopy, FileFilter filter,
private void copyFrameworkModule(boolean recopy, FileFilter filter,
File src, File dest) throws IOException {
if (src != null) {
if (recopy && dest.exists()) {
@ -501,7 +494,7 @@ public class HeliumBundleFactory {
}
}
void deleteYarnCache() {
private void deleteYarnCache() {
FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
@ -664,7 +657,7 @@ public class HeliumBundleFactory {
private void npmCommand(String args, Map<String, String> env) throws TaskRunnerException {
NpmRunner npm = frontEndPluginFactory.getNpmRunner(
getProxyConfig(isSecure(defaultNpmRegistryUrl)), defaultNpmRegistryUrl);
getProxyConfig(isSecure(defaultNpmInstallerUrl)), defaultNpmInstallerUrl);
npm.execute(args, env);
}
@ -679,7 +672,7 @@ public class HeliumBundleFactory {
private void yarnCommand(FrontendPluginFactory fpf,
String args, Map<String, String> env) throws TaskRunnerException {
YarnRunner yarn = fpf.getYarnRunner(
getProxyConfig(isSecure(defaultNpmRegistryUrl)), defaultNpmRegistryUrl);
getProxyConfig(isSecure(defaultNpmInstallerUrl)), defaultNpmInstallerUrl);
yarn.execute(args, env);
}

View file

@ -28,13 +28,15 @@ import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.google.gson.annotations.SerializedName;
import com.google.gson.internal.StringMap;
import org.apache.zeppelin.dep.Dependency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.zeppelin.dep.Dependency;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
import static org.apache.zeppelin.notebook.utility.IdHashes.generateId;
@ -413,27 +415,45 @@ public class InterpreterSetting {
runtimeInfosToBeCleared = null;
}
// For backward compatibility of interpreter.json format after ZEPPELIN-2654
public void convertPermissionsFromUsersToOwners(JsonObject jsonObject) {
if (jsonObject != null) {
JsonObject option = jsonObject.getAsJsonObject("option");
if (option != null) {
JsonArray users = option.getAsJsonArray("users");
if (users != null) {
if (this.option.getOwners() == null) {
this.option.owners = new LinkedList<>();
}
for (JsonElement user : users) {
this.option.getOwners().add(user.getAsString());
}
}
}
}
}
// For backward compatibility of interpreter.json format after ZEPPELIN-2403
public void convertFlatPropertiesToPropertiesWithWidgets() {
StringMap newProperties = new StringMap();
if (properties != null && properties instanceof StringMap) {
StringMap p = (StringMap) properties;
if (properties != null && properties instanceof StringMap) {
StringMap p = (StringMap) properties;
for (Object o : p.entrySet()) {
Map.Entry entry = (Map.Entry) o;
if (!(entry.getValue() instanceof StringMap)) {
StringMap newProperty = new StringMap();
newProperty.put("name", entry.getKey());
newProperty.put("value", entry.getValue());
newProperty.put("type", InterpreterPropertyType.TEXTAREA.getValue());
newProperties.put(entry.getKey().toString(), newProperty);
} else {
// already converted
return;
for (Object o : p.entrySet()) {
Map.Entry entry = (Map.Entry) o;
if (!(entry.getValue() instanceof StringMap)) {
StringMap newProperty = new StringMap();
newProperty.put("name", entry.getKey());
newProperty.put("value", entry.getValue());
newProperty.put("type", InterpreterPropertyType.TEXTAREA.getValue());
newProperties.put(entry.getKey().toString(), newProperty);
} else {
// already converted
return;
}
}
}
this.properties = newProperties;
}
this.properties = newProperties;
}
}
}

View file

@ -17,6 +17,9 @@
package org.apache.zeppelin.interpreter;
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
@ -71,6 +74,8 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.internal.StringMap;
import com.google.gson.reflect.TypeToken;
@ -161,8 +166,9 @@ public class InterpreterSettingManager {
InterpreterInfoSaving infoSaving;
try (BufferedReader json =
Files.newBufferedReader(interpreterBindingPath, StandardCharsets.UTF_8)) {
infoSaving = gson.fromJson(json, InterpreterInfoSaving.class);
JsonParser jsonParser = new JsonParser();
JsonObject jsonObject = jsonParser.parse(json).getAsJsonObject();
infoSaving = gson.fromJson(jsonObject.toString(), InterpreterInfoSaving.class);
for (String k : infoSaving.interpreterSettings.keySet()) {
InterpreterSetting setting = infoSaving.interpreterSettings.get(k);
@ -185,13 +191,16 @@ public class InterpreterSettingManager {
properties.put(key, new InterpreterProperty(key, fields.get("value"), type));
}
setting.setProperties(properties);
// Always use separate interpreter process
// While we decided to turn this feature on always (without providing
// enable/disable option on GUI).
// previously created setting should turn this feature on here.
setting.getOption().setRemote(true);
setting.convertPermissionsFromUsersToOwners(
jsonObject.getAsJsonObject("interpreterSettings").getAsJsonObject(setting.getId()));
// Update transient information from InterpreterSettingRef
InterpreterSetting interpreterSettingObject =
interpreterSettingsRef.get(setting.getGroup());

View file

@ -342,17 +342,13 @@ public class Paragraph extends Job implements Serializable, Cloneable {
return null;
}
private boolean hasPermission(String user, List<String> intpUsers) {
if (1 > intpUsers.size()) {
private boolean hasPermission(List<String> userAndRoles, List<String> intpUsersAndRoles) {
if (1 > intpUsersAndRoles.size()) {
return true;
}
for (String u : intpUsers) {
if (user.trim().equals(u.trim())) {
return true;
}
}
return false;
Set<String> intersection = new HashSet<>(intpUsersAndRoles);
intersection.retainAll(userAndRoles);
return (intpUsersAndRoles.isEmpty() || (intersection.size() > 0));
}
public boolean isBlankParagraph() {
@ -441,12 +437,12 @@ public class Paragraph extends Job implements Serializable, Cloneable {
}
private boolean interpreterHasUser(InterpreterSetting intp) {
return intp.getOption().permissionIsSet() && intp.getOption().getUsers() != null;
return intp.getOption().permissionIsSet() && intp.getOption().getOwners() != null;
}
private boolean isUserAuthorizedToAccessInterpreter(InterpreterOption intpOpt) {
return intpOpt.permissionIsSet() && hasPermission(authenticationInfo.getUser(),
intpOpt.getUsers());
return intpOpt.permissionIsSet() && hasPermission(authenticationInfo.getUsersAndRoles(),
intpOpt.getOwners());
}
private InterpreterSetting getInterpreterSettingById(String id) {

View file

@ -27,13 +27,18 @@ import java.util.Properties;
*/
public class Util {
private static final String PROJECT_PROPERTIES_VERSION_KEY = "version";
private static final String GIT_PROPERTIES_COMMIT_ID_KEY = "git.commit.id.abbrev";
private static final String GIT_PROPERTIES_COMMIT_TS_KEY = "git.commit.time";
private static Properties projectProperties;
private static Properties gitProperties;
static {
projectProperties = new Properties();
gitProperties = new Properties();
try {
projectProperties.load(Util.class.getResourceAsStream("/project.properties"));
gitProperties.load(Util.class.getResourceAsStream("/git.properties"));
} catch (IOException e) {
//Fail to read project.properties
}
@ -48,4 +53,24 @@ public class Util {
return StringUtils.defaultIfEmpty(projectProperties.getProperty(PROJECT_PROPERTIES_VERSION_KEY),
StringUtils.EMPTY);
}
/**
* Get Zeppelin Git latest commit id
*
* @return Latest Zeppelin commit id
*/
public static String getGitCommitId() {
return StringUtils.defaultIfEmpty(gitProperties.getProperty(GIT_PROPERTIES_COMMIT_ID_KEY),
StringUtils.EMPTY);
}
/**
* Get Zeppelin Git latest commit timestamp
*
* @return Latest Zeppelin commit timestamp
*/
public static String getGitTimestamp() {
return StringUtils.defaultIfEmpty(gitProperties.getProperty(GIT_PROPERTIES_COMMIT_TS_KEY),
StringUtils.EMPTY);
}
}

View file

@ -119,7 +119,6 @@ public class HeliumBundleFactoryTest {
assertEquals(lastModified, bundle.lastModified());
}
@Test
public void bundleLocalPackage() throws IOException, TaskRunnerException {
URL res = Resources.getResource("helium/webpack.config.js");

View file

@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.zeppelin.util;
import org.junit.Test;
import static org.junit.Assert.assertNotNull;
public class UtilTest {
@Test
public void getVersionTest() {
assertNotNull(Util.getVersion());
}
@Test
public void getGitInfoTest() {
assertNotNull(Util.getGitCommitId());
assertNotNull(Util.getGitTimestamp());
}
}