Merge branch 'master' into extends-zrun-remote-transaction

This commit is contained in:
CloverHearts 2016-11-23 22:38:01 +09:00
commit 8a54917987
34 changed files with 650 additions and 211 deletions

View file

@ -23,7 +23,9 @@ cache:
- .spark-dist
- ${HOME}/.m2
- ${HOME}/R
- .node_modules
- zeppelin-web/node
- zeppelin-web/node_modules
- zeppelin-web/bower_components
addons:
apt:
@ -65,6 +67,7 @@ matrix:
env: TEST_SELENIUM="true" SCALA_VER="2.10" SPARK_VER="1.6.1" HADOOP_VER="2.3" PROFILE="-Pspark-1.6 -Phadoop-2.3 -Ppyspark -Pexamples" BUILD_FLAG="package -DskipTests -DskipRat" TEST_FLAG="verify -DskipRat" TEST_PROJECTS="-pl zeppelin-interpreter,zeppelin-zengine,zeppelin-server,zeppelin-display,spark-dependencies,spark -Dtest=org.apache.zeppelin.AbstractFunctionalSuite -DfailIfNoTests=false"
before_install:
- echo "MAVEN_OPTS='-Xms1024M -Xmx2048M -XX:MaxPermSize=1024m -XX:-UseGCOverheadLimit'" >> ~/.mavenrc
- "ls -la .spark-dist ${HOME}/.m2/repository/.cache/maven-download-plugin"
- ls .node_modules && cp -r .node_modules zeppelin-web/node_modules || echo "node_modules are not cached"
- mkdir -p ~/R
@ -75,7 +78,7 @@ before_install:
- ./dev/change_scala_version.sh $SCALA_VER
install:
- mvn $BUILD_FLAG $PROFILE -B
- mvn -Dorg.slf4j.simpleLogger.defaultLogLevel=warn $BUILD_FLAG $PROFILE -B
before_script:
- travis_retry ./testing/downloadSpark.sh $SPARK_VER $HADOOP_VER
@ -84,7 +87,6 @@ before_script:
script:
- mvn -Dorg.slf4j.simpleLogger.defaultLogLevel=warn $TEST_FLAG $PROFILE -B $TEST_PROJECTS
- rm -rf .node_modules; cp -r zeppelin-web/node_modules .node_modules
after_success:
- echo "Travis exited with ${TRAVIS_TEST_RESULT}"

View file

@ -190,7 +190,7 @@
<property>
<name>zeppelin.interpreters</name>
<value>org.apache.zeppelin.spark.SparkInterpreter,org.apache.zeppelin.spark.PySparkInterpreter,org.apache.zeppelin.rinterpreter.RRepl,org.apache.zeppelin.rinterpreter.KnitR,org.apache.zeppelin.spark.SparkRInterpreter,org.apache.zeppelin.spark.SparkSqlInterpreter,org.apache.zeppelin.spark.DepInterpreter,org.apache.zeppelin.markdown.Markdown,org.apache.zeppelin.angular.AngularInterpreter,org.apache.zeppelin.shell.ShellInterpreter,org.apache.zeppelin.file.HDFSFileInterpreter,org.apache.zeppelin.flink.FlinkInterpreter,,org.apache.zeppelin.python.PythonInterpreter,org.apache.zeppelin.lens.LensInterpreter,org.apache.zeppelin.ignite.IgniteInterpreter,org.apache.zeppelin.ignite.IgniteSqlInterpreter,org.apache.zeppelin.cassandra.CassandraInterpreter,org.apache.zeppelin.geode.GeodeOqlInterpreter,org.apache.zeppelin.postgresql.PostgreSqlInterpreter,org.apache.zeppelin.jdbc.JDBCInterpreter,org.apache.zeppelin.kylin.KylinInterpreter,org.apache.zeppelin.elasticsearch.ElasticsearchInterpreter,org.apache.zeppelin.scalding.ScaldingInterpreter,org.apache.zeppelin.alluxio.AlluxioInterpreter,org.apache.zeppelin.hbase.HbaseInterpreter,org.apache.zeppelin.livy.LivySparkInterpreter,org.apache.zeppelin.livy.LivyPySparkInterpreter,org.apache.zeppelin.livy.LivySparkRInterpreter,org.apache.zeppelin.livy.LivySparkSQLInterpreter,org.apache.zeppelin.bigquery.BigQueryInterpreter,org.apache.zeppelin.beam.BeamInterpreter,org.apache.zeppelin.pig.PigInterpreter,org.apache.zeppelin.pig.PigQueryInterpreter,org.apache.zeppelin.scio.ScioInterpreter</value>
<value>org.apache.zeppelin.spark.SparkInterpreter,org.apache.zeppelin.spark.PySparkInterpreter,org.apache.zeppelin.rinterpreter.RRepl,org.apache.zeppelin.rinterpreter.KnitR,org.apache.zeppelin.spark.SparkRInterpreter,org.apache.zeppelin.spark.SparkSqlInterpreter,org.apache.zeppelin.spark.DepInterpreter,org.apache.zeppelin.markdown.Markdown,org.apache.zeppelin.angular.AngularInterpreter,org.apache.zeppelin.shell.ShellInterpreter,org.apache.zeppelin.file.HDFSFileInterpreter,org.apache.zeppelin.flink.FlinkInterpreter,,org.apache.zeppelin.python.PythonInterpreter,org.apache.zeppelin.python.PythonInterpreterPandasSql,org.apache.zeppelin.python.PythonCondaInterpreter,org.apache.zeppelin.lens.LensInterpreter,org.apache.zeppelin.ignite.IgniteInterpreter,org.apache.zeppelin.ignite.IgniteSqlInterpreter,org.apache.zeppelin.cassandra.CassandraInterpreter,org.apache.zeppelin.geode.GeodeOqlInterpreter,org.apache.zeppelin.postgresql.PostgreSqlInterpreter,org.apache.zeppelin.jdbc.JDBCInterpreter,org.apache.zeppelin.kylin.KylinInterpreter,org.apache.zeppelin.elasticsearch.ElasticsearchInterpreter,org.apache.zeppelin.scalding.ScaldingInterpreter,org.apache.zeppelin.alluxio.AlluxioInterpreter,org.apache.zeppelin.hbase.HbaseInterpreter,org.apache.zeppelin.livy.LivySparkInterpreter,org.apache.zeppelin.livy.LivyPySparkInterpreter,org.apache.zeppelin.livy.LivySparkRInterpreter,org.apache.zeppelin.livy.LivySparkSQLInterpreter,org.apache.zeppelin.bigquery.BigQueryInterpreter,org.apache.zeppelin.beam.BeamInterpreter,org.apache.zeppelin.pig.PigInterpreter,org.apache.zeppelin.pig.PigQueryInterpreter,org.apache.zeppelin.scio.ScioInterpreter</value>
<description>Comma separated interpreter configurations. First interpreter become a default</description>
</property>

View file

@ -95,7 +95,7 @@ Congratulations, you have successfully installed Apache Zeppelin! Here are few s
* Check [JDBC Interpreter](../interpreter/jdbc.html) to know more about configure and uses multiple JDBC data sources.
#### Zeppelin with Python ...
* Check [Python interpreter](../interpreter/python.html) to know more about Matplotlib, Pandas integration.
* Check [Python interpreter](../interpreter/python.html) to know more about Matplotlib, Pandas, Conda integration.
#### Multi-user environment ...

View file

@ -59,6 +59,31 @@ To access the help, type **help()**
## Python modules
The interpreter can use all modules already installed (with pip, easy_install...)
## Conda
[Conda](http://conda.pydata.org/) is an package management system and environment management system for python.
`%python.conda` interpreter lets you change between environments.
#### Usage
List your environments
```
%python.conda
```
Activate an environment
```
%python.conda activate [ENVIRONMENT_NAME]
```
Deactivate
```
%python.conda deactivate
```
## Using Zeppelin Dynamic Forms
You can leverage [Zeppelin Dynamic Form]({{BASE_PATH}}/manual/dynamicform.html) inside your Python code.

View file

@ -334,7 +334,7 @@
"progressUpdateIntervalMs": 500
}
],
"name": "Zeppelin Tutorial",
"name": "Zeppelin Tutorial/Basic Features (Spark)",
"id": "2A94M5J1Z",
"angularObjects": {
"2B6FF8NNU": [],
@ -344,4 +344,4 @@
"looknfeel": "default"
},
"info": {}
}
}

View file

@ -1037,7 +1037,7 @@
"progressUpdateIntervalMs": 500
}
],
"name": "R Tutorial",
"name": "Zeppelin Tutorial/R (SparkR)",
"id": "2BWJFTXKJ",
"lastReplName": {
"value": ""
@ -1066,4 +1066,4 @@
"looknfeel": "default"
},
"info": {}
}
}

View file

@ -743,7 +743,7 @@
"progressUpdateIntervalMs": 500
}
],
"name": "Mahout Tutorial",
"name": "Zeppelin Tutorial/Using Mahout",
"id": "2BYEZ5EVK",
"angularObjects": {
"2BWWSVNY7:shared_process": [],
@ -769,4 +769,4 @@
},
"config": {},
"info": {}
}
}

View file

@ -669,7 +669,7 @@
"progressUpdateIntervalMs": 500
}
],
"name": "Zeppelin Tutorial: Python - matplotlib basic",
"name": "Zeppelin Tutorial/Matplotlib (Python • PySpark)",
"id": "2C2AUG798",
"angularObjects": {
"2BWWUCUVW:shared_process": [],
@ -693,4 +693,4 @@
},
"config": {},
"info": {}
}
}

View file

@ -437,7 +437,7 @@
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<version>3.0.0</version>
</plugin>
<plugin>

View file

@ -0,0 +1,192 @@
/*
* 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.python;
import org.apache.zeppelin.interpreter.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Conda support
*/
public class PythonCondaInterpreter extends Interpreter {
Logger logger = LoggerFactory.getLogger(PythonCondaInterpreter.class);
Pattern condaEnvListPattern = Pattern.compile("([^\\s]*)[\\s*]*\\s(.*)");
Pattern listPattern = Pattern.compile("env\\s*list\\s?");
Pattern activatePattern = Pattern.compile("activate\\s*(.*)");
Pattern deactivatePattern = Pattern.compile("deactivate");
Pattern helpPattern = Pattern.compile("help");
String pythonCommand = null;
public PythonCondaInterpreter(Properties property) {
super(property);
}
@Override
public void open() {
}
@Override
public void close() {
}
@Override
public InterpreterResult interpret(String st, InterpreterContext context) {
InterpreterOutput out = context.out;
Matcher listMatcher = listPattern.matcher(st);
Matcher activateMatcher = activatePattern.matcher(st);
Matcher deactivateMatcher = deactivatePattern.matcher(st);
Matcher helpMatcher = helpPattern.matcher(st);
if (st == null || st.isEmpty() || listMatcher.matches()) {
listEnv(out);
return new InterpreterResult(InterpreterResult.Code.SUCCESS);
} else if (activateMatcher.matches()) {
String envName = activateMatcher.group(1);
pythonCommand = "conda run -n " + envName + " \"python -iu\"";
restartPythonProcess();
return new InterpreterResult(InterpreterResult.Code.SUCCESS, "\"" + envName + "\" activated");
} else if (deactivateMatcher.matches()) {
pythonCommand = null;
restartPythonProcess();
return new InterpreterResult(InterpreterResult.Code.SUCCESS, "Deactivated");
} else if (helpMatcher.matches()) {
printUsage(out);
return new InterpreterResult(InterpreterResult.Code.SUCCESS);
} else {
return new InterpreterResult(InterpreterResult.Code.ERROR, "Not supported command: " + st);
}
}
private void restartPythonProcess() {
PythonInterpreter python = getPythonInterpreter();
python.close();
python.open();
}
protected PythonInterpreter getPythonInterpreter() {
LazyOpenInterpreter lazy = null;
PythonInterpreter python = null;
Interpreter p = getInterpreterInTheSameSessionByClassName(PythonInterpreter.class.getName());
while (p instanceof WrappedInterpreter) {
if (p instanceof LazyOpenInterpreter) {
lazy = (LazyOpenInterpreter) p;
}
p = ((WrappedInterpreter) p).getInnerInterpreter();
}
python = (PythonInterpreter) p;
if (lazy != null) {
lazy.open();
}
return python;
}
public String getPythonCommand() {
return pythonCommand;
}
private void listEnv(InterpreterOutput out) {
StringBuilder sb = createStringBuilder();
try {
int exit = runCommand(sb, "conda", "env", "list");
if (exit == 0) {
out.setType(InterpreterResult.Type.HTML);
out.write("<h4>Conda environments</h4>\n");
// start table
out.write("<div style=\"display:table\">\n");
String[] lines = sb.toString().split("\n");
for (String s : lines) {
if (s == null || s.isEmpty() || s.startsWith("#")) {
continue;
}
Matcher match = condaEnvListPattern.matcher(s);
if (!match.matches()) {
continue;
}
out.write(String.format("<div style=\"display:table-row\">" +
"<div style=\"display:table-cell;width:150px\">%s</div>" +
"<div style=\"display:table-cell;\">%s</div>" +
"</div>\n",
match.group(1), match.group(2)));
}
// end table
out.write("</div><br />\n");
out.write("<small><code>%python.conda help</code> for the usage</small>\n");
} else {
out.write("Failed to run 'conda' " + exit + "\n");
}
} catch (IOException | InterruptedException e) {
throw new InterpreterException(e);
}
}
private void printUsage(InterpreterOutput out) {
try {
out.setType(InterpreterResult.Type.HTML);
out.writeResource("output_templates/usage.html");
} catch (IOException e) {
logger.error("Can't print usage", e);
}
}
@Override
public void cancel(InterpreterContext context) {
}
@Override
public FormType getFormType() {
return FormType.NONE;
}
@Override
public int getProgress(InterpreterContext context) {
return 0;
}
protected int runCommand(StringBuilder sb, String ... command)
throws IOException, InterruptedException {
ProcessBuilder builder = new ProcessBuilder(command);
builder.redirectErrorStream(true);
Process process = builder.start();
InputStream stdout = process.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(stdout));
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
sb.append("\n");
}
int r = process.waitFor(); // Let the process finish.
return r;
}
protected StringBuilder createStringBuilder() {
return new StringBuilder();
}
}

View file

@ -120,6 +120,7 @@ public class PythonInterpreter extends Interpreter {
try {
if (process != null) {
process.close();
process = null;
}
if (gatewayServer != null) {
gatewayServer.shutdown();
@ -198,7 +199,12 @@ public class PythonInterpreter extends Interpreter {
public PythonProcess getPythonProcess() {
if (process == null) {
return new PythonProcess(getProperty(ZEPPELIN_PYTHON));
PythonCondaInterpreter conda = getCondaInterpreter();
String binPath = getProperty(ZEPPELIN_PYTHON);
if (conda != null && conda.getPythonCommand() != null) {
binPath = conda.getPythonCommand();
}
return new PythonProcess(binPath);
} else {
return process;
}
@ -278,5 +284,25 @@ public class PythonInterpreter extends Interpreter {
public int getMaxResult() {
return maxResult;
}
private PythonCondaInterpreter getCondaInterpreter() {
LazyOpenInterpreter lazy = null;
PythonCondaInterpreter conda = null;
Interpreter p = getInterpreterInTheSameSessionByClassName(
PythonCondaInterpreter.class.getName());
while (p instanceof WrappedInterpreter) {
if (p instanceof LazyOpenInterpreter) {
lazy = (LazyOpenInterpreter) p;
}
p = ((WrappedInterpreter) p).getInnerInterpreter();
}
conda = (PythonCondaInterpreter) p;
if (lazy != null) {
lazy.open();
}
return conda;
}
}

View file

@ -49,7 +49,23 @@ public class PythonProcess {
}
public void open() throws IOException {
ProcessBuilder builder = new ProcessBuilder(binPath, "-iu");
ProcessBuilder builder;
boolean hasParams = binPath.split(" ").length > 1;
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
if (hasParams) {
builder = new ProcessBuilder(binPath.split(" "));
} else {
builder = new ProcessBuilder(binPath, "-iu");
}
} else {
String cmd;
if (hasParams) {
cmd = binPath;
} else {
cmd = binPath + " -iu";
}
builder = new ProcessBuilder("bash", "-c", cmd);
}
builder.redirectErrorStream(true);
process = builder.start();

View file

@ -32,5 +32,16 @@
"language": "sql",
"editOnDblClick": false
}
},
{
"group": "python",
"name": "conda",
"className": "org.apache.zeppelin.python.PythonCondaInterpreter",
"properties": {
},
"editor":{
"language": "sh",
"editOnDblClick": false
}
}
]

View file

@ -0,0 +1,27 @@
<!--
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.
-->
<h4>Usage</h4>
<div>
Activate an environment (python interpreter will be restarted)
<pre>%python.conda activate [ENV NAME]</pre>
</div>
<div>
Deactivate
<pre>%python.conda deactivate</pre>
</div>
<div>
List the Conda environments
<pre>%python.conda</pre>
</div>

View file

@ -0,0 +1,116 @@
/*
* 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.python;
import org.apache.zeppelin.display.GUI;
import org.apache.zeppelin.interpreter.*;
import org.apache.zeppelin.user.AuthenticationInfo;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Properties;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
public class PythonCondaInterpreterTest implements InterpreterOutputListener {
private PythonCondaInterpreter conda;
private PythonInterpreter python;
@Before
public void setUp() {
conda = spy(new PythonCondaInterpreter(new Properties()));
python = mock(PythonInterpreter.class);
InterpreterGroup group = new InterpreterGroup();
group.put("note", Arrays.asList(python, conda));
python.setInterpreterGroup(group);
conda.setInterpreterGroup(group);
doReturn(python).when(conda).getPythonInterpreter();
}
@Test
public void testListEnv() throws IOException, InterruptedException {
InterpreterContext context = getInterpreterContext();
StringBuilder sb = new StringBuilder();
sb.append("#comment\n\nenv1 * /path1\nenv2\t/path2\n");
doReturn(sb).when(conda).createStringBuilder();
doReturn(0).when(conda)
.runCommand(any(StringBuilder.class), anyString(), anyString(), anyString());
// list available env
InterpreterResult result = conda.interpret("", context);
assertEquals(InterpreterResult.Code.SUCCESS, result.code());
String out = new String(context.out.toByteArray());
assertTrue(out.contains(">env1<"));
assertTrue(out.contains(">/path1<"));
assertTrue(out.contains(">env2<"));
assertTrue(out.contains(">/path2<"));
}
@Test
public void testActivateEnv() {
InterpreterContext context = getInterpreterContext();
conda.interpret("activate env", context);
verify(python, times(1)).open();
verify(python, times(1)).close();
assertEquals("conda run -n env \"python -iu\"", conda.getPythonCommand());
}
@Test
public void testDeactivate() {
InterpreterContext context = getInterpreterContext();
conda.interpret("deactivate", context);
verify(python, times(1)).open();
verify(python, times(1)).close();
assertEquals(null, conda.getPythonCommand());
}
private InterpreterContext getInterpreterContext() {
return new InterpreterContext(
"noteId",
"paragraphId",
"paragraphTitle",
"paragraphText",
new AuthenticationInfo(),
new HashMap<String, Object>(),
new GUI(),
null,
null,
null,
new InterpreterOutput(this));
}
@Override
public void onAppend(InterpreterOutput out, byte[] line) {
}
@Override
public void onUpdate(InterpreterOutput out, byte[] output) {
}
}

View file

@ -37,9 +37,13 @@ import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.LinkedList;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.apache.zeppelin.interpreter.ClassloaderInterpreter;
import org.apache.zeppelin.interpreter.Interpreter;
import org.apache.zeppelin.interpreter.InterpreterGroup;
import org.apache.zeppelin.interpreter.InterpreterResult;
import org.junit.Before;
import org.junit.Test;
@ -82,8 +86,15 @@ public class PythonInterpreterTest {
}
});
// python interpreter
pythonInterpreter = spy(new PythonInterpreter(getPythonTestProperties()));
// create interpreter group
InterpreterGroup group = new InterpreterGroup();
group.put("note", new LinkedList<Interpreter>());
group.get("note").add(pythonInterpreter);
pythonInterpreter.setInterpreterGroup(group);
when(pythonInterpreter.getPythonProcess()).thenReturn(mockPythonProcess);
when(mockPythonProcess.sendAndGetResult(eq("\n\nimport py4j\n"))).thenReturn("ImportError");
}
@ -220,15 +231,24 @@ public class PythonInterpreterTest {
@Test
public void checkMultiRowErrorFails() {
PythonInterpreter pythonInterpreter = new PythonInterpreter(
PythonInterpreterTest.getPythonTestProperties()
);
// create interpreter group
InterpreterGroup group = new InterpreterGroup();
group.put("note", new LinkedList<Interpreter>());
group.get("note").add(pythonInterpreter);
pythonInterpreter.setInterpreterGroup(group);
pythonInterpreter.open();
String codeRaiseException = "raise Exception(\"test exception\")";
InterpreterResult ret = pythonInterpreter.interpret(codeRaiseException, null);
assertNotNull("Interpreter result for raise exception is Null", ret);
System.err.println("ret = '" + ret + "'");
assertEquals(InterpreterResult.Code.ERROR, ret.code());
assertTrue(ret.message().length() > 0);

View file

@ -21,9 +21,13 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.apache.zeppelin.interpreter.Interpreter;
import org.apache.zeppelin.interpreter.InterpreterGroup;
import org.apache.zeppelin.interpreter.InterpreterResult;
import org.junit.Test;
import java.util.Arrays;
/**
* Python interpreter unit test that user real Python
*
@ -46,6 +50,11 @@ public class PythonInterpreterWithPythonInstalledTest {
//given
PythonInterpreter realPython = new PythonInterpreter(
PythonInterpreterTest.getPythonTestProperties());
// create interpreter group
InterpreterGroup group = new InterpreterGroup();
group.put("note", Arrays.asList((Interpreter) realPython));
realPython.setInterpreterGroup(group);
realPython.open();
//when
@ -65,6 +74,9 @@ public class PythonInterpreterWithPythonInstalledTest {
//given
PythonInterpreter realPython = new PythonInterpreter(
PythonInterpreterTest.getPythonTestProperties());
InterpreterGroup group = new InterpreterGroup();
group.put("note", Arrays.asList((Interpreter) realPython));
realPython.setInterpreterGroup(group);
realPython.open();
//when
@ -84,6 +96,9 @@ public class PythonInterpreterWithPythonInstalledTest {
//given
PythonInterpreter realPython = new PythonInterpreter(
PythonInterpreterTest.getPythonTestProperties());
InterpreterGroup group = new InterpreterGroup();
group.put("note", Arrays.asList((Interpreter) realPython));
realPython.setInterpreterGroup(group);
realPython.open();
//when

View file

@ -504,5 +504,4 @@
</profile>
</profiles>
</project>

View file

@ -571,6 +571,20 @@ public class NotebookServer extends WebSocketServlet implements
broadcast(noteId, new Message(OP.INTERPRETER_BINDINGS).put("interpreterBindings", settingList));
}
public void broadcastParagraph(Note note, Paragraph p) {
broadcast(note.getId(), new Message(OP.PARAGRAPH).put("paragraph", p));
}
private void broadcastNewParagraph(Note note, Paragraph para) {
LOG.info("Broadcasting paragraph on run call instead of note.");
int paraIndex = note.getParagraphs().indexOf(para);
broadcast(
note.getId(),
new Message(OP.PARAGRAPH_ADDED)
.put("paragraph", para)
.put("index", paraIndex));
}
public void broadcastNoteList(AuthenticationInfo subject, HashSet userAndRoles) {
if (subject == null) {
subject = new AuthenticationInfo(StringUtils.EMPTY);
@ -718,7 +732,10 @@ public class NotebookServer extends WebSocketServlet implements
AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
note.persist(subject);
broadcastNote(note);
broadcast(note.getId(), new Message(OP.NOTE_UPDATED)
.put("name", name)
.put("config", config)
.put("info", note.getInfo()));
broadcastNoteList(subject, userAndRoles);
}
}
@ -860,7 +877,7 @@ public class NotebookServer extends WebSocketServlet implements
p.setTitle((String) fromMessage.get("title"));
p.setText((String) fromMessage.get("paragraph"));
note.persist(subject);
broadcast(note.getId(), new Message(OP.PARAGRAPH).put("paragraph", p));
broadcastParagraph(note, p);;
}
private void cloneNote(NotebookSocket conn, HashSet<String> userAndRoles,
@ -933,9 +950,12 @@ public class NotebookServer extends WebSocketServlet implements
/** We dont want to remove the last paragraph */
if (!note.isLastParagraph(paragraphId)) {
note.removeParagraph(subject.getUser(), paragraphId);
Paragraph para = note.removeParagraph(subject.getUser(), paragraphId);
note.persist(subject);
broadcastNote(note);
if (para != null) {
broadcast(note.getId(), new Message(OP.PARAGRAPH_REMOVED).
put("id", para.getId()));
}
}
}
@ -953,9 +973,9 @@ public class NotebookServer extends WebSocketServlet implements
userAndRoles, notebookAuthorization.getWriters(noteId));
return;
}
note.clearParagraphOutput(paragraphId);
broadcastNote(note);
Paragraph paragraph = note.getParagraph(paragraphId);
broadcastParagraph(note, paragraph);
}
private void completion(NotebookSocket conn, HashSet<String> userAndRoles, Notebook notebook,
@ -1240,7 +1260,9 @@ public class NotebookServer extends WebSocketServlet implements
note.moveParagraph(paragraphId, newIndex);
note.persist(subject);
broadcastNote(note);
broadcast(note.getId(), new Message(OP.PARAGRAPH_MOVED)
.put("id", paragraphId)
.put("index", newIndex));
}
private void insertParagraph(NotebookSocket conn, HashSet<String> userAndRoles,
@ -1257,9 +1279,9 @@ public class NotebookServer extends WebSocketServlet implements
return;
}
note.insertParagraph(index);
Paragraph newPara = note.insertParagraph(index);
note.persist(subject);
broadcastNote(note);
broadcastNewParagraph(note, newPara);
}
private void cancelParagraph(NotebookSocket conn, HashSet<String> userAndRoles, Notebook notebook,
@ -1316,7 +1338,8 @@ public class NotebookServer extends WebSocketServlet implements
boolean isTheLastParagraph = note.isLastParagraph(p.getId());
if (!(text.trim().equals(p.getMagic()) || Strings.isNullOrEmpty(text)) &&
isTheLastParagraph) {
note.addParagraph();
Paragraph newPara = note.addParagraph();
broadcastNewParagraph(note, newPara);
}
AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
@ -1700,8 +1723,9 @@ public class NotebookServer extends WebSocketServlet implements
LOG.error(e.toString(), e);
}
}
notebookServer.broadcastNote(note);
if (job instanceof Paragraph) {
notebookServer.broadcastParagraph(note, (Paragraph) job);
}
try {
notebookServer.broadcastUpdateNoteJobInfo(System.currentTimeMillis() - 5000);
} catch (IOException e) {

View file

@ -440,8 +440,9 @@ public class ParagraphActionsIT extends AbstractZeppelinIT {
waitForParagraph(1, "READY");
driver.findElement(By.xpath(getParagraphXPath(1) + "//textarea")).sendKeys(Keys.SHIFT + "5");
driver.findElement(By.xpath(getParagraphXPath(1) + "//textarea")).sendKeys("md" + Keys.ENTER);
setTextOfParagraph(1, "%md");
driver.findElement(By.xpath(getParagraphXPath(1) + "//textarea")).sendKeys(Keys.ARROW_RIGHT);
driver.findElement(By.xpath(getParagraphXPath(1) + "//textarea")).sendKeys(Keys.ENTER);
driver.findElement(By.xpath(getParagraphXPath(1) + "//textarea")).sendKeys(Keys.SHIFT + "3");
driver.findElement(By.xpath(getParagraphXPath(1) + "//textarea")).sendKeys(" abc");

View file

@ -1,51 +1,57 @@
# Zeppelin Web Application
This is Zeppelin's frontend project.
## Development Guide
## Compile Zeppelin web
### Packaging
### New environment
If you want to package the zeppelin-web only, simply run this command in this folder.
This will download all the dependencies including node (the binaries in the folder `zeppelin-web/node`)
If you want to compile the WebApplication only, you will have to simply run `mvn package` in this folder.
```
$ mvn package
```
This will Download all the dependencies including node js and npm (you will find the binaries in the folder `zeppelin-web/node`).
### Local Development
We are supposed to provide some **helper script** for __bower__ and __grunt__, but they are currently outdated, so you might want install them on your machine and use them instead.
It is recommended to install node 6.0.0+ since Zeppelin uses 6.9.1+ (see [creationix/nvm](https://github.com/creationix/nvm))
### Configured environment
All build commands are described in [package.json](./package.json)
Here are the basic commands to compile the WebApplication with a configured environment (Installed grunt, bower, npm)
```sh
# install required depepdencies and bower packages (only once)
$ npm install
**Build the application for production**
# build zeppelin-web for production
$ npm run build
`./grunt build`
# run frontend application only in dev mode (localhost:9000)
# you need to run zeppelin backend instance also
$ npm run start
**Run the application in dev mode**
# execute tests
$ npm run test
```
``./grunt serve``
## Troubleshooting
This will launch a Zeppelin WebApplication on port **9000** that will update on code changes.
(You will need to have Zeppelin running on the side)
#### Troubleshooting
**git error**
#### Git error
In case of the error `ECMDERR Failed to execute "git ls-remote --tags --heads git://xxxxx", exit code of #128`
change your git config with `git config --global url."https://".insteadOf git://`
**proxy issues**
#### Proxy issues
Try to add to the `.bowerrc` file the following content:
```
"proxy" : "http://<host>:<port>",
"https-proxy" : "http://<host>:<port>"
```
```
also try to add proxy info to npm install command:
```
```xml
<execution>
<id>npm install</id>
<goals>
@ -57,8 +63,8 @@ also try to add proxy info to npm install command:
</execution>
```
and retry to build again.
## Contribute on Zeppelin Web
If you wish to help us and contribute to Zeppelin WebApplication, please look at the overall project [contribution guidelines](https://zeppelin.apache.org/contribution/contributions.html) and the more focused [Zeppelin WebApplication's documentation](https://zeppelin.apache.org/contribution/webapplication.html).

View file

@ -1,17 +0,0 @@
#
# 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.
#
"node/node" "./node_modules/bower/bin/bower" "$@"

View file

@ -1,18 +0,0 @@
#
# 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.
#
"node/node" "./node_modules/.bin/grunt" "$@"

View file

@ -2,13 +2,20 @@
"name": "zeppelin-web",
"license": "Apache-2.0",
"version": "0.0.0",
"engines" : { "node" : ">=6.0.0" },
"scripts": {
"postinstall": "node_modules/.bin/bower install --silent",
"build": "./node_modules/.bin/grunt build",
"start": "./node_modules/.bin/grunt serve",
"test": "./node_modules/.bin/grunt test"
},
"dependencies": {
"grunt-angular-templates": "^0.5.7",
"grunt-dom-munger": "^3.4.0"
},
"devDependencies": {
"autoprefixer": "^6.1.0",
"bower": "1.7.2",
"bower": "^1.8.0",
"grunt": "^0.4.1",
"grunt-cache-bust": "1.3.0",
"grunt-cli": "^0.1.13",
@ -26,17 +33,18 @@
"grunt-goog-webfont-dl": "^0.1.2",
"grunt-htmlhint": "^0.9.13",
"grunt-jscs": "^2.1.0",
"grunt-karma": "~0.8.3",
"grunt-karma": "~2.0.0",
"grunt-newer": "^0.7.0",
"grunt-ng-annotate": "^0.10.0",
"grunt-postcss": "^0.7.1",
"grunt-svgmin": "^0.4.0",
"grunt-usemin": "^2.1.1",
"grunt-wiredep": "~2.0.0",
"karma": "~0.12.23",
"karma-coverage": "^0.5.1",
"karma-jasmine": "~0.1.5",
"karma-phantomjs-launcher": "~0.1.4",
"jasmine-core": "^2.5.2",
"karma": "~1.3.0",
"karma-coverage": "^1.1.1",
"karma-jasmine": "~1.0.2",
"karma-phantomjs-launcher": "~1.0.2",
"load-grunt-tasks": "^0.4.0",
"time-grunt": "^0.3.1"
},

View file

@ -90,7 +90,7 @@
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>0.0.25</version>
<version>1.3</version>
<executions>
<execution>
@ -99,8 +99,8 @@
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v0.12.13</nodeVersion>
<npmVersion>2.15.0</npmVersion>
<nodeVersion>v6.9.1</nodeVersion>
<npmVersion>3.10.8</npmVersion>
</configuration>
</execution>
@ -109,36 +109,29 @@
<goals>
<goal>npm</goal>
</goals>
</execution>
<execution>
<id>bower install</id>
<goals>
<goal>bower</goal>
</goals>
<configuration>
<arguments>--allow-root install</arguments>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>grunt build</id>
<id>npm build</id>
<goals>
<goal>grunt</goal>
<goal>npm</goal>
</goals>
<configuration>
<arguments>build</arguments>
<arguments>run build</arguments>
</configuration>
</execution>
<execution>
<id>grunt test</id>
<id>npm test</id>
<goals>
<goal>grunt</goal>
<goal>npm</goal>
</goals>
<phase>test</phase>
<configuration>
<arguments>test</arguments>
<arguments>run test</arguments>
</configuration>
</execution>

View file

@ -360,87 +360,51 @@
return noteCopy;
};
var updateNote = function(note) {
/** update Note name */
if (note.name !== $scope.note.name) {
console.log('change note name: %o to %o', $scope.note.name, note.name);
$scope.note.name = note.name;
}
$scope.note.config = note.config;
$scope.note.info = note.info;
var newParagraphIds = note.paragraphs.map(function(x) {return x.id;});
var oldParagraphIds = $scope.note.paragraphs.map(function(x) {return x.id;});
var numNewParagraphs = newParagraphIds.length;
var numOldParagraphs = oldParagraphIds.length;
var paragraphToBeFocused;
var focusedParagraph;
for (var i = 0; i < $scope.note.paragraphs.length; i++) {
var paragraphId = $scope.note.paragraphs[i].id;
if (angular.element('#' + paragraphId + '_paragraphColumn_main').scope().paragraphFocused) {
focusedParagraph = paragraphId;
break;
var addPara = function(paragraph, index) {
$scope.note.paragraphs.splice(index, 0, paragraph);
_.each($scope.note.paragraphs, function(para) {
if (para.id === paragraph.id) {
para.focus = true;
}
}
/** add a new paragraph */
if (numNewParagraphs > numOldParagraphs) {
for (var index in newParagraphIds) {
if (oldParagraphIds[index] !== newParagraphIds[index]) {
$scope.note.paragraphs.splice(index, 0, note.paragraphs[index]);
paragraphToBeFocused = note.paragraphs[index].id;
break;
}
$scope.$broadcast('updateParagraph', {
note: $scope.note, // pass the note object to paragraph scope
paragraph: note.paragraphs[index]});
}
}
/** update or move paragraph */
if (numNewParagraphs === numOldParagraphs) {
for (var idx in newParagraphIds) {
var newEntry = note.paragraphs[idx];
if (oldParagraphIds[idx] === newParagraphIds[idx]) {
$scope.$broadcast('updateParagraph', {
note: $scope.note, // pass the note object to paragraph scope
paragraph: newEntry});
} else {
// move paragraph
var oldIdx = oldParagraphIds.indexOf(newParagraphIds[idx]);
$scope.note.paragraphs.splice(oldIdx, 1);
$scope.note.paragraphs.splice(idx, 0, newEntry);
// rebuild id list since paragraph has moved.
oldParagraphIds = $scope.note.paragraphs.map(function(x) {return x.id;});
}
if (focusedParagraph === newParagraphIds[idx]) {
paragraphToBeFocused = focusedParagraph;
}
}
}
/** remove paragraph */
if (numNewParagraphs < numOldParagraphs) {
for (var oldidx in oldParagraphIds) {
if (oldParagraphIds[oldidx] !== newParagraphIds[oldidx]) {
$scope.note.paragraphs.splice(oldidx, 1);
break;
}
}
}
// restore focus of paragraph
for (var f = 0; f < $scope.note.paragraphs.length; f++) {
if (paragraphToBeFocused === $scope.note.paragraphs[f].id) {
$scope.note.paragraphs[f].focus = true;
}
}
});
};
var removePara = function(paragraphId) {
var removeIdx;
_.each($scope.note.paragraphs, function(para, idx) {
if (para.id === paragraphId) {
removeIdx = idx;
}
});
return $scope.note.paragraphs.splice(removeIdx, 1);
};
$scope.$on('addParagraph', function(event, paragraph, index) {
addPara(paragraph, index);
});
$scope.$on('removeParagraph', function(event, paragraphId) {
removePara(paragraphId);
});
$scope.$on('moveParagraph', function(event, paragraphId, newIdx) {
var removedPara = removePara(paragraphId);
if (removedPara && removedPara.length === 1) {
addPara(removedPara[0], newIdx);
}
});
$scope.$on('updateNote', function(event, name, config, info) {
/** update Note name */
if (name !== $scope.note.name) {
console.log('change note name to : %o', $scope.note.name);
$scope.note.name = name;
}
$scope.note.config = config;
$scope.note.info = info;
initializeLookAndFeel();
});
var getInterpreterBindings = function() {
websocketMsgSrv.getInterpreterBindings($scope.note.id);
};
@ -856,8 +820,6 @@
if ($scope.note === null) {
$scope.note = note;
} else {
updateNote(note);
}
initializeLookAndFeel();
//open interpreter binding setting when there're none selected

View file

@ -12,11 +12,11 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<a class="notebook-list-item" ng-if="note.id" href="#/notebook/{{note.id}}">
<a class="notebook-list-item" ng-if="!note.children" href="#/notebook/{{note.id}}">
<i style="font-size: 10px; margin-right: 5px;" class="icon-doc"></i>
<span>{{noteName(note)}}</span>
</a>
<li ng-if="!note.id" ng-click="$event.stopPropagation()">
<li ng-if="note.children" ng-click="$event.stopPropagation()">
<expand-collapse>
<div>
<a class="notebook-list-item" href="javascript:void(0)">

View file

@ -140,6 +140,14 @@
$rootScope.$broadcast('configurationsInfo', data);
} else if (op === 'INTERPRETER_SETTINGS') {
$rootScope.$broadcast('interpreterSettings', data);
} else if (op === 'PARAGRAPH_ADDED') {
$rootScope.$broadcast('addParagraph', data.paragraph, data.index);
} else if (op === 'PARAGRAPH_REMOVED') {
$rootScope.$broadcast('removeParagraph', data.id);
} else if (op === 'PARAGRAPH_MOVED') {
$rootScope.$broadcast('moveParagraph', data.id, data.index);
} else if (op === 'NOTE_UPDATED') {
$rootScope.$broadcast('updateNote', data.name, data.config, data.info);
}
});

View file

@ -61,7 +61,7 @@ describe('Controller: ParagraphCtrl', function() {
it('should call loadTableData() and getGraphMode() should return "table" when the result type is "TABLE"',
function() {
scope.getResultType = jasmine.createSpy('getResultType spy').andCallFake(function() {
scope.getResultType = jasmine.createSpy('getResultType spy').and.callFake(function() {
return 'TABLE';
});
spyOn(scope, 'setGraphMode');
@ -71,7 +71,7 @@ describe('Controller: ParagraphCtrl', function() {
});
it('should call renderHtml() when the result type is "HTML"', function() {
scope.getResultType = jasmine.createSpy('getResultType spy').andCallFake(function() {
scope.getResultType = jasmine.createSpy('getResultType spy').and.callFake(function() {
return 'HTML';
});
spyOn(scope, 'renderHtml');
@ -80,7 +80,7 @@ describe('Controller: ParagraphCtrl', function() {
});
it('should call renderAngular() when the result type is "ANGULAR"', function() {
scope.getResultType = jasmine.createSpy('getResultType spy').andCallFake(function() {
scope.getResultType = jasmine.createSpy('getResultType spy').and.callFake(function() {
return 'ANGULAR';
});
spyOn(scope, 'renderAngular');

View file

@ -537,6 +537,7 @@ public class ZeppelinConfiguration extends XMLConfiguration {
+ "org.apache.zeppelin.flink.FlinkInterpreter,"
+ "org.apache.zeppelin.python.PythonInterpreter,"
+ "org.apache.zeppelin.python.PythonInterpreterPandasSql,"
+ "org.apache.zeppelin.python.PythonCondaInterpreter,"
+ "org.apache.zeppelin.ignite.IgniteInterpreter,"
+ "org.apache.zeppelin.ignite.IgniteSqlInterpreter,"
+ "org.apache.zeppelin.lens.LensInterpreter,"

View file

@ -1381,9 +1381,9 @@ public class InterpreterFactory implements InterpreterGroupFactory {
public Map<String, Object> getEditorSetting(String user, String noteId, String replName) {
Interpreter intp = getInterpreter(user, noteId, replName);
Map<String, Object> editor = DEFAULT_EDITOR;
String defaultSettingName = getDefaultInterpreterSetting(noteId).getName();
String group = StringUtils.EMPTY;
try {
String defaultSettingName = getDefaultInterpreterSetting(noteId).getName();
List<InterpreterSetting> intpSettings = getInterpreterSettings(noteId);
for (InterpreterSetting intpSetting : intpSettings) {
String[] replNameSplit = replName.split("\\.");

View file

@ -157,24 +157,25 @@ public class Paragraph extends Job implements Serializable, Cloneable {
return null;
}
String trimmed = text.trim();
if (!trimmed.startsWith("%")) {
return null;
}
// get script head
int scriptHeadIndex = 0;
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (Character.isWhitespace(ch) || ch == '(') {
scriptHeadIndex = i;
for (int i = 0; i < trimmed.length(); i++) {
char ch = trimmed.charAt(i);
if (Character.isWhitespace(ch) || ch == '(' || ch == '\n') {
break;
}
scriptHeadIndex = i;
}
if (scriptHeadIndex == 0) {
return null;
}
String head = text.substring(0, scriptHeadIndex);
if (head.startsWith("%")) {
return head.substring(1);
} else {
if (scriptHeadIndex < 1) {
return null;
}
String head = text.substring(1, scriptHeadIndex + 1);
return head;
}
public String getScriptBody() {
@ -190,10 +191,12 @@ public class Paragraph extends Job implements Serializable, Cloneable {
if (magic == null) {
return text;
}
if (magic.length() + 1 >= text.length()) {
String trimmed = text.trim();
if (magic.length() + 1 >= trimmed.length()) {
return "";
}
return text.substring(magic.length() + 1).trim();
return trimmed.substring(magic.length() + 1).trim();
}
public Interpreter getRepl(String name) {

View file

@ -147,6 +147,10 @@ public class Message {
INTERPRETER_SETTINGS, // [s-c] interpreter settings
ERROR_INFO, // [s-c] error information to be sent
WATCHER, // [s-c] Change websocket to watcher mode.
PARAGRAPH_ADDED, // [s-c] paragraph is added
PARAGRAPH_REMOVED, // [s-c] paragraph deleted
PARAGRAPH_MOVED, // [s-c] paragraph moved
NOTE_UPDATED // [s-c] paragraph updated(name, config)
}
public static final Message EMPTY = new Message(null);

View file

@ -52,6 +52,21 @@ public class ParagraphTest {
assertEquals(text, Paragraph.getScriptBody(text));
}
@Test
public void replNameAndNoBody() {
String text = "%md";
assertEquals("md", Paragraph.getRequiredReplName(text));
assertEquals("", Paragraph.getScriptBody(text));
}
@Test
public void replSingleCharName() {
String text = "%r a";
assertEquals("r", Paragraph.getRequiredReplName(text));
assertEquals("a", Paragraph.getScriptBody(text));
}
@Test
public void replNameEndsWithWhitespace() {
String text = "%md\r\n###Hello";