diff --git a/docs/displaysystem/angular.md b/docs/displaysystem/angular.md index c8a0ddc1542..c1752dce110 100644 --- a/docs/displaysystem/angular.md +++ b/docs/displaysystem/angular.md @@ -96,3 +96,28 @@ z.angularBind("run", 0) will initialize 'run' to zero. And then register watcher After clicked button, you'll see both 'run' and numWatched are increased by 1 + + + +
+#### Bind/Unbind function + +Through ZeppelinContext, you can bind/unbind function to AngularJS view. + +Currently it only works in Spark Interpreter (scala). + +``` +// bind function in current notebook. +z.angularBindFunction(String name, { args: Seq[Object] => ... }) + +// bind function in all notebooks related to current interpreter. +z.angularBindGlobal(String name, Object object) + +// unbind function in current notebook. +z.angularUnbind(String name) + +// unbind function in all notebooks related to current interpreter. +z.angularUnbindGlobal(String name) + +``` + diff --git a/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java b/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java index 0d2d50c0e21..c9a11f5ea9f 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java +++ b/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java @@ -24,21 +24,14 @@ import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import org.apache.spark.SparkContext; import org.apache.spark.sql.SQLContext; import org.apache.spark.sql.SQLContext.QueryExecution; import org.apache.spark.sql.catalyst.expressions.Attribute; import org.apache.spark.sql.hive.HiveContext; -import org.apache.zeppelin.display.AngularObject; -import org.apache.zeppelin.display.AngularObjectRegistry; -import org.apache.zeppelin.display.AngularObjectWatcher; -import org.apache.zeppelin.display.GUI; +import org.apache.zeppelin.display.*; import org.apache.zeppelin.display.Input.ParamOption; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterContextRunner; @@ -48,6 +41,9 @@ import scala.Tuple2; import scala.Unit; import scala.collection.Iterable; +import scala.collection.JavaConversions; +import scala.collection.Seq; +import scala.collection.mutable.Set; /** * Spark context for zeppelin. @@ -749,4 +745,55 @@ private void angularUnbind(String name, String noteId) { AngularObjectRegistry registry = interpreterContext.getAngularObjectRegistry(); registry.remove(name, noteId); } + + + public void angularBindFunction(String name, final scala.Function1, Unit> func) { + String noteId = interpreterContext.getNoteId(); + angularBindFunction(name, noteId, new AngularFunctionRunnable() { + @Override + public void run(Object... args) { + Set sets = JavaConversions.asScalaSet(new HashSet(Arrays.asList(args))); + func.apply(sets.toSeq()); + } + }); + } + + public void angularBindFunction(String name, AngularFunctionRunnable func) { + String noteId = interpreterContext.getNoteId(); + angularBindFunction(name, noteId, func); + } + + public void angularUnbindFunction(String name) { + String noteId = interpreterContext.getNoteId(); + angularUnbindFunction(name, noteId); + } + + public void angularBindFunctionGlobal(String name, + final scala.Function1, Unit> func) { + angularBindFunction(name, null, new AngularFunctionRunnable() { + @Override + public void run(Object... args) { + Set sets = JavaConversions.asScalaSet(new HashSet(Arrays.asList(args))); + func.apply(sets.toSeq()); + } + }); + } + + public void angularBindFunctionGlobal(String name, AngularFunctionRunnable func) { + angularBindFunction(name, null, func); + } + + public void angularUnbindFunctionGlobal(String name) { + angularUnbindFunction(name, null); + } + + private void angularBindFunction(String name, String noteId, AngularFunctionRunnable func) { + AngularObjectRegistry registry = interpreterContext.getAngularObjectRegistry(); + registry.createAngularFunction(name, noteId, func); + } + + private void angularUnbindFunction(String name, String noteId) { + AngularObjectRegistry registry = interpreterContext.getAngularObjectRegistry(); + registry.removeAngularFunction(name, noteId); + } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularFunction.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularFunction.java new file mode 100644 index 00000000000..f01018fa9b2 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularFunction.java @@ -0,0 +1,83 @@ +/* + * 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.display; + +import com.google.gson.Gson; +import org.apache.zeppelin.interpreter.InterpreterContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; + +/** + * AngularFunction provides proxy object in front-end side. + * Calling front-end side proxy will invoke AngularFunctionRunnable of this AngularFunction + */ +public class AngularFunction extends AngularObjectWatcher { + Logger logger = LoggerFactory.getLogger(AngularFunction.class); + private static final String ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = "_Z_ANGULAR_FUNC_"; + private final AngularObjectRegistry registry; + private final String name; + private final String noteId; + private final AngularFunctionRunnable runnable; + + // arguments of invocation from fron-end proxy function + AngularObject angularObject; + + protected AngularFunction(AngularObjectRegistry registry, + String name, String noteId, + AngularFunctionRunnable runnable) { + super(null); + this.name = name; + this.noteId = noteId; + this.runnable = runnable; + this.registry = registry; + + remove(); + + angularObject = registry.add(getFuncName(name), "", noteId); + angularObject.addWatcher(this); + } + + + static String getFuncName(String name) { + return ANGULAR_FUNCTION_OBJECT_NAME_PREFIX + name; + } + + @Override + public void watch(Object oldObject, Object newObject, InterpreterContext context) { + if (runnable == null) { + return; + } + + Object argumentList = angularObject.get(); + if (argumentList instanceof Object[]) { + runnable.run((Object[]) angularObject.get()); + } else { + runnable.run(angularObject.get()); + } + } + + + void remove() { + registry.remove(getFuncName(name), noteId); + } + + public String getNoteId() { + return noteId; + } +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularFunctionRunnable.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularFunctionRunnable.java new file mode 100644 index 00000000000..43c8ff863d4 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularFunctionRunnable.java @@ -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.display; + +/** + * User provided function body for AngularFunction + */ +public interface AngularFunctionRunnable { + public void run (Object ... args); +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistry.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistry.java index d6bab7b732c..fb83a37c034 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistry.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObjectRegistry.java @@ -31,7 +31,7 @@ * - localRegistry: AngularObject is valid only inside of a single notebook */ public class AngularObjectRegistry { - Map> registry = + Map> registry = new HashMap>(); private final String GLOBAL_KEY = "_GLOBAL_"; private AngularObjectRegistryListener listener; @@ -69,6 +69,15 @@ public AngularObject add(String name, Object o, String noteId) { return add(name, o, noteId, true); } + public AngularFunction createAngularFunction(String name, String noteId, + AngularFunctionRunnable func) { + return new AngularFunction(this, name, noteId, func); + } + + public void removeAngularFunction(String name, String noteId) { + remove(AngularFunction.getFuncName(name), noteId); + } + private String getRegistryKey(String noteId) { if (noteId == null) { return GLOBAL_KEY; @@ -87,7 +96,7 @@ private Map getRegistryForKey(String noteId) { return registry.get(key); } } - + public AngularObject add(String name, Object o, String noteId, boolean emit) { AngularObject ao = createNewAngularObject(name, o, noteId); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/ZeppelinIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/ZeppelinIT.java index 6b464dd3965..835e6896e61 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/ZeppelinIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/ZeppelinIT.java @@ -18,6 +18,7 @@ package org.apache.zeppelin; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; @@ -99,7 +100,16 @@ boolean waitForParagraph(final int paragraphNo, final String state) { boolean waitForText(final String txt, final By locator) { try { WebElement element = pollingWait(locator, MAX_BROWSER_TIMEOUT_SEC); - return txt.equals(element.getText()); + FluentWait wait = new FluentWait(element) + .withTimeout(MAX_BROWSER_TIMEOUT_SEC, TimeUnit.SECONDS) + .pollingEvery(1, TimeUnit.SECONDS); + + return wait.until(new Function() { + @Override + public Boolean apply(WebElement webElement) { + return txt.equals(webElement.getText()); + } + }); } catch (TimeoutException e) { return false; } @@ -112,9 +122,9 @@ public WebElement pollingWait(final By locator, final long timeWait) { .ignoring(NoSuchElementException.class); return wait.until(new Function() { - public WebElement apply(WebDriver driver) { - return driver.findElement(locator); - } + public WebElement apply(WebDriver driver) { + return driver.findElement(locator); + } }); }; @@ -147,8 +157,8 @@ public void testAngularDisplay() throws InterruptedException{ waitForParagraph(1, "FINISHED"); // check expected text - waitForText("BindingTest__", By.xpath( - getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]")); + assertTrue(waitForText("BindingTest__", By.xpath( + getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]"))); /* * Bind variable @@ -161,8 +171,8 @@ public void testAngularDisplay() throws InterruptedException{ waitForParagraph(2, "FINISHED"); // check expected text - waitForText("BindingTest_1_", By.xpath( - getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]")); + assertTrue(waitForText("BindingTest_1_", By.xpath( + getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]"))); /* @@ -187,8 +197,8 @@ public void testAngularDisplay() throws InterruptedException{ getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]")).click(); // check expected text - waitForText("BindingTest_2_", By.xpath( - getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]")); + assertTrue(waitForText("BindingTest_2_", By.xpath( + getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]"))); /* * Register watcher @@ -218,8 +228,8 @@ public void testAngularDisplay() throws InterruptedException{ waitForParagraph(3, "FINISHED"); // check expected text by watcher - waitForText("myVar=3", By.xpath( - getParagraphXPath(3) + "//div[@ng-bind=\"paragraph.result.msg\"]")); + assertTrue(waitForText("myVar=3", By.xpath( + getParagraphXPath(3) + "//div[@ng-bind=\"paragraph.result.msg\"]"))); /* * Unbind @@ -232,8 +242,8 @@ public void testAngularDisplay() throws InterruptedException{ waitForParagraph(5, "FINISHED"); // check expected text - waitForText("BindingTest__", - By.xpath(getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]")); + assertTrue(waitForText("BindingTest__", + By.xpath(getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]"))); /* * Bind again and see rebind works. @@ -243,8 +253,8 @@ public void testAngularDisplay() throws InterruptedException{ waitForParagraph(2, "FINISHED"); // check expected text - waitForText("BindingTest_1_", - By.xpath(getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]")); + assertTrue(waitForText("BindingTest_1_", + By.xpath(getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]"))); driver.findElement(By.xpath("//*[@id='main']/div//h3/span[1]/button[@tooltip='Remove the notebook']")) .sendKeys(Keys.ENTER); @@ -256,7 +266,117 @@ public void testAngularDisplay() throws InterruptedException{ System.out.println("testCreateNotebook Test executed"); } catch (ElementNotVisibleException e) { File scrFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE); + throw e; + } + } + + + @Test + public void testAngularFunction() throws InterruptedException{ + if (!endToEndTestEnabled()) { + return; + } + try { + createNewNote(); + + // wait for first paragraph's " READY " status text + waitForParagraph(1, "READY"); + + /* + * print angular template + * %angular
BindingTest_{{myVar}}_
+ */ + WebElement paragraph1Editor = driver.findElement(By.xpath(getParagraphXPath(1) + "//textarea")); + paragraph1Editor.sendKeys("println" + Keys.chord(Keys.SHIFT, "9") + "\"" + + Keys.chord(Keys.SHIFT, "5") + + "angular
" + + "FunctionTest
\")"); + paragraph1Editor.sendKeys(Keys.chord(Keys.SHIFT, Keys.ENTER)); + waitForParagraph(1, "FINISHED"); + /* + * Bind variable + * var a=1 + * z.angularFunction("myFunc", { args: Seq[Object] => a = a + 1 }) + */ + assertEquals(1, driver.findElements(By.xpath(getParagraphXPath(2) + "//textarea")).size()); + WebElement paragraph2Editor = driver.findElement(By.xpath(getParagraphXPath(2) + "//textarea")); + paragraph2Editor.sendKeys("var a=1; z.angularBindFunction" + Keys.chord(Keys.SHIFT, "9") + "\"myFunc\", " + + "{ args: Seq[Object] => a = a " + Keys.chord(Keys.ADD) + " 1 })"); + paragraph2Editor.sendKeys(Keys.chord(Keys.SHIFT, Keys.ENTER)); + waitForParagraph(2, "FINISHED"); + + /* + * print variable + * print(a) + */ + WebElement paragraph3Editor = driver.findElement(By.xpath(getParagraphXPath(3) + "//textarea")); + paragraph3Editor.sendKeys("print" + Keys.chord(Keys.SHIFT, "9") + "a)"); + paragraph3Editor.sendKeys(Keys.chord(Keys.SHIFT, Keys.ENTER)); + waitForParagraph(3, "FINISHED"); + + // check expected text + assertTrue(waitForText("1", By.xpath( + getParagraphXPath(3) + "//div[@ng-bind=\"paragraph.result.msg\"]"))); + + + /* + * Click button + */ + driver.findElement(By.xpath( + getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]")).click(); + + /** + * Run paragarph3 + */ + paragraph3Editor.sendKeys(Keys.chord(Keys.SHIFT, Keys.ENTER)); + waitForParagraph(3, "FINISHED"); + + // check expected text + assertTrue(waitForText("2", By.xpath( + getParagraphXPath(3) + "//div[@ng-bind=\"paragraph.result.msg\"]"))); + + + /* + * unbind function + * z.angularUnbindFunction("myFunc") + */ + WebElement paragraph4Editor = driver.findElement(By.xpath(getParagraphXPath(4) + "//textarea")); + paragraph4Editor.sendKeys("z.angularUnbindFunction" + Keys.chord(Keys.SHIFT, "9") + "\"myFunc\")"); + paragraph4Editor.sendKeys(Keys.chord(Keys.SHIFT, Keys.ENTER)); + waitForParagraph(4, "FINISHED"); + + + /* + * Click button + */ + driver.findElement(By.xpath( + getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]")).click(); + + /** + * Run paragarph3 + */ + paragraph3Editor.sendKeys(Keys.chord(Keys.SHIFT, Keys.ENTER)); + waitForParagraph(3, "FINISHED"); + + // check expected text + assertTrue(waitForText("2", By.xpath( + getParagraphXPath(3) + "//div[@ng-bind=\"paragraph.result.msg\"]"))); + + + // remove notebook + driver.findElement(By.xpath("//*[@id='main']/div//h3/span[1]/button[@tooltip='Remove the notebook']")) + .sendKeys(Keys.ENTER); + ZeppelinITUtils.sleep(1000, true); + driver.findElement(By.xpath("//div[@class='modal-dialog'][contains(.,'delete this notebook')]" + + "//div[@class='modal-footer']//button[contains(.,'OK')]")).click(); + ZeppelinITUtils.sleep(100, true); + + System.out.println("testCreateNotebook Test executed"); + } catch (ElementNotVisibleException e) { + File scrFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE); + throw e; } } diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 9030a49979d..0e5216f1049 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -18,6 +18,8 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $route, $routeParams, $location, $rootScope, $http, websocketMsgSrv, baseUrlSrv, $timeout, SaveAsService) { + + var ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_'; $scope.note = null; $scope.showEditor = false; $scope.editorToggled = false; @@ -655,6 +657,17 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', }); } scope[varName] = data.angularObject.object; + + // create proxy for AngularFunction + if (varName.startsWith(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX)) { + var funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length); + scope[funcName] = function() { + scope[varName] = arguments; + console.log('angular function invoked %o', arguments); + }; + + console.log('angular function created %o', scope[funcName]); + } } }); @@ -671,7 +684,12 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', // remove scope variable scope[varName] = undefined; + + // remove proxy for AngularFunction + if (varName.startsWith(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX)) { + var funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length); + scope[funcName] = undefined; + } } }); - });