From 33663941c5ca544b0922d7093c8a118d8c3fce3f Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Mon, 17 Apr 2017 17:08:41 +0900 Subject: [PATCH 01/12] [ZEPPELIN-2315] (bug) new note.json is overwritten by old note.json (master, branch-0.7) ### What is this PR for? - `note.json` is converted every time - as a result, changes in `note.json` is overwritten by old `note.json` Occurs in **0.8.0-SNAPSHOT** and **branch-0.7** ### What type of PR is it? [Bug Fix] ### Todos NONE ### What is the Jira issue? [ZEPPELIN-2315](https://issues.apache.org/jira/browse/ZEPPELIN-2315) ### How should this be tested? 1. create a note in 0.6.0 which including graph 2. migrate it to 0.7.0+ (just copy dir) 3. open in Zeppelin 4. click other graph type rather than table (e.g `scatter chart`) 5. restart 7. should see the last change (graph) is persisted ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? - NO * Is there breaking changes for older versions? - NO * Does this needs documentation? - NO Author: 1ambda <1amb4a@gmail.com> Closes #2256 from 1ambda/ZEPPELIN-2315/should-convert-old-notebook-only-once and squashes the following commits: 4e77c0a8b [1ambda] fix: Convert old notebook format only once --- .../src/main/java/org/apache/zeppelin/notebook/Notebook.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index e7f7e49a43..de48befab3 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java @@ -414,6 +414,10 @@ public class Notebook implements NoteEventListener { public void convertFromSingleResultToMultipleResultsFormat(Note note) { for (Paragraph p : note.paragraphs) { Object ret = p.getPreviousResultFormat(); + if (ret != null && p.results != null) { + continue; // already converted + } + try { if (ret != null && ret instanceof Map) { Map r = ((Map) ret); From a7ffc1291885cc555ed84bd71f455ebcb2e9dd62 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 11 Apr 2017 18:09:35 +0800 Subject: [PATCH 02/12] ZEPPELIN-2390. Improve returnType for z.checkbox ### What is this PR for? Currently it is not convenient to access the individual item of the return value of z.checkbox, I would propose to return `Seq` for `SparkInterpreter` and `list` for `PySparkInterpreter`. This might cause some incompatibility, but I think it is acceptable considering the benefits. Besides that, before this PR, all the items of checkbox would be checked by default in `PySparkInterpreter` which is inconsistent with `SparkInterpreter`, so I change it to nothing is selected by default in this PR. ### What type of PR is it? [Improvement] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2390 ### How should this be tested? Unit test is added ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2241 from zjffdu/ZEPPELIN-2390 and squashes the following commits: 75e3afc [Jeff Zhang] ZEPPELIN-2390. Improve returnType for z.checkbox --- .../zeppelin/spark/ZeppelinContext.java | 9 ++-- .../main/resources/python/zeppelin_pyspark.py | 10 ++-- .../java/org/apache/zeppelin/display/GUI.java | 4 +- .../rest/ZeppelinSparkClusterTest.java | 54 ++++++++++++++++--- 4 files changed, 61 insertions(+), 16 deletions(-) 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 6e96d9d2b4..e18791592a 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java +++ b/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java @@ -136,7 +136,7 @@ public class ZeppelinContext { } @ZeppelinApi - public scala.collection.Iterable checkbox(String name, + public scala.collection.Seq checkbox(String name, scala.collection.Iterable> options) { List allChecked = new LinkedList<>(); for (Tuple2 option : asJavaIterable(options)) { @@ -146,11 +146,12 @@ public class ZeppelinContext { } @ZeppelinApi - public scala.collection.Iterable checkbox(String name, + public scala.collection.Seq checkbox(String name, scala.collection.Iterable defaultChecked, scala.collection.Iterable> options) { - return collectionAsScalaIterable(gui.checkbox(name, asJavaCollection(defaultChecked), - tuplesToParamOptions(options))); + return scala.collection.JavaConversions.asScalaBuffer( + gui.checkbox(name, asJavaCollection(defaultChecked), + tuplesToParamOptions(options))).toSeq(); } private ParamOption[] tuplesToParamOptions( diff --git a/spark/src/main/resources/python/zeppelin_pyspark.py b/spark/src/main/resources/python/zeppelin_pyspark.py index 5029d59c27..da4d7943e7 100644 --- a/spark/src/main/resources/python/zeppelin_pyspark.py +++ b/spark/src/main/resources/python/zeppelin_pyspark.py @@ -97,13 +97,15 @@ class PyZeppelinContext(dict): def checkbox(self, name, options, defaultChecked=None): if defaultChecked is None: - defaultChecked = list(map(lambda items: items[0], options)) + defaultChecked = [] optionTuples = list(map(lambda items: self.__tupleToScalaTuple2(items), options)) optionIterables = gateway.jvm.scala.collection.JavaConversions.collectionAsScalaIterable(optionTuples) defaultCheckedIterables = gateway.jvm.scala.collection.JavaConversions.collectionAsScalaIterable(defaultChecked) - - checkedIterables = self.z.checkbox(name, defaultCheckedIterables, optionIterables) - return gateway.jvm.scala.collection.JavaConversions.asJavaCollection(checkedIterables) + checkedItems = gateway.jvm.scala.collection.JavaConversions.seqAsJavaList(self.z.checkbox(name, defaultCheckedIterables, optionIterables)) + result = [] + for checkedItem in checkedItems: + result.append(checkedItem) + return result; def registerHook(self, event, cmd, replName=None): if replName is None: diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java index 30a8ba7799..40ce8caeaf 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java @@ -75,14 +75,14 @@ public class GUI implements Serializable { return value; } - public Collection checkbox(String id, Collection defaultChecked, + public List checkbox(String id, Collection defaultChecked, ParamOption[] options) { Collection checked = (Collection) params.get(id); if (checked == null) { checked = defaultChecked; } forms.put(id, new Input(id, defaultChecked, "checkbox", options)); - Collection filtered = new LinkedList<>(); + List filtered = new LinkedList<>(); for (Object o : checked) { if (isValidOption(o, options)) { filtered.add(o); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java index 9bcdbfa875..eb8186e756 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java @@ -483,18 +483,19 @@ public class ZeppelinSparkClusterTest extends AbstractTestRestApi { } @Test - public void testZeppelinContextDynamicForms() throws IOException { + public void testSparkZeppelinContextDynamicForms() throws IOException { Note note = ZeppelinServer.notebook.createNote(anonymous); Paragraph p = note.addParagraph(AuthenticationInfo.ANONYMOUS); note.setName("note"); Map config = p.getConfig(); config.put("enabled", true); p.setConfig(config); - String code = "%spark.spark z.input(\"my_input\", \"default_name\")\n" + - "z.select(\"my_select\", \"select_2\"," + - "Seq((\"1\", \"select_1\"), (\"2\", \"select_2\")))\n" + - "z.checkbox(\"my_checkbox\", Seq(\"check_1\"), " + - "Seq((\"1\", \"check_1\"), (\"2\", \"check_2\")))"; + String code = "%spark.spark println(z.input(\"my_input\", \"default_name\"))\n" + + "println(z.select(\"my_select\", \"1\"," + + "Seq((\"1\", \"select_1\"), (\"2\", \"select_2\"))))\n" + + "val items=z.checkbox(\"my_checkbox\", Seq(\"2\"), " + + "Seq((\"1\", \"check_1\"), (\"2\", \"check_2\")))\n" + + "println(items(0))"; p.setText(code); p.setAuthenticationInfo(anonymous); note.run(p.getId()); @@ -505,5 +506,46 @@ public class ZeppelinSparkClusterTest extends AbstractTestRestApi { assert(formIter.next().equals("my_input")); assert(formIter.next().equals("my_select")); assert(formIter.next().equals("my_checkbox")); + + // check dynamic forms values + String[] result = p.getResult().message().get(0).getData().split("\n"); + assertEquals(4, result.length); + assertEquals("default_name", result[0]); + assertEquals("1", result[1]); + assertEquals("items: Seq[Object] = Buffer(2)", result[2]); + assertEquals("2", result[3]); + } + + @Test + public void testPySparkZeppelinContextDynamicForms() throws IOException { + Note note = ZeppelinServer.notebook.createNote(anonymous); + Paragraph p = note.addParagraph(AuthenticationInfo.ANONYMOUS); + note.setName("note"); + Map config = p.getConfig(); + config.put("enabled", true); + p.setConfig(config); + String code = "%spark.pyspark print(z.input('my_input', 'default_name'))\n" + + "print(z.select('my_select', " + + "[('1', 'select_1'), ('2', 'select_2')], defaultValue='1'))\n" + + "items=z.checkbox('my_checkbox', " + + "[('1', 'check_1'), ('2', 'check_2')], defaultChecked=['2'])\n" + + "print(items[0])"; + p.setText(code); + p.setAuthenticationInfo(anonymous); + note.run(p.getId()); + waitForFinish(p); + + assertEquals(Status.FINISHED, p.getStatus()); + Iterator formIter = p.settings.getForms().keySet().iterator(); + assert(formIter.next().equals("my_input")); + assert(formIter.next().equals("my_select")); + assert(formIter.next().equals("my_checkbox")); + + // check dynamic forms values + String[] result = p.getResult().message().get(0).getData().split("\n"); + assertEquals(3, result.length); + assertEquals("default_name", result[0]); + assertEquals("1", result[1]); + assertEquals("2", result[2]); } } From c706d453e06a2f8c71fcd61d7e4dddc87cafc0f4 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Tue, 11 Apr 2017 14:21:53 +0800 Subject: [PATCH 03/12] ZEPPELIN-2386. Add parameter for check running current paragraph in ZeppelinContext ### What is this PR for? I can reproduce it via the test code in ZeppelinIT.testAngularDisplay. First run the 4 paragraphs to initiate the state. Then when I click paragraph 0 the first time, paragraph 2 will run correctly. But if I click paragraph 0 again, interpreter would raise the following exception. The cause is that ZeppelinContext's interpreterContext now point to paragraph 2, because paragraph 2 is the last paragraph that runs in this interpreter. So I propose to add parameter for check running current paragraph in ZeppelinContext. * Add 2 methods in ZeppelinContext. `run(idx, checkCurrentParagraph) `, 'run(noteId, idx, context, checkCurrentParagraph)' * Also make some changes for debug logging. ### What type of PR is it? [Bug Fix | Improvement] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2386 ### How should this be tested? Tested manually and also add integration test. ### Screenshots (if appropriate) Before ![zeppelin_before](https://cloud.githubusercontent.com/assets/164491/24895529/8514826e-1ec3-11e7-915b-70baf09c297a.gif) After ![zeppelin_after](https://cloud.githubusercontent.com/assets/164491/24895532/88e87bc0-1ec3-11e7-90ee-a7ca7a3758f8.gif) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2240 from zjffdu/ZEPPELIN-2386 and squashes the following commits: 03995a3 [Jeff Zhang] ZEPPELIN-2386. Add parameter to check running current paragraph in ZeppelinContext --- .../zeppelin/spark/ZeppelinContext.java | 60 ++++++++++++++++--- .../zeppelin/display/AngularObject.java | 4 +- .../zeppelin/socket/NotebookServer.java | 9 +-- .../zeppelin/integration/ZeppelinIT.java | 17 +++++- 4 files changed, 77 insertions(+), 13 deletions(-) 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 e18791592a..7e1ab704b3 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java +++ b/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java @@ -312,7 +312,7 @@ public class ZeppelinContext { */ @ZeppelinApi public void run(String noteId, String paragraphId) { - run(noteId, paragraphId, interpreterContext); + run(noteId, paragraphId, interpreterContext, true); } /** @@ -321,8 +321,27 @@ public class ZeppelinContext { */ @ZeppelinApi public void run(String paragraphId) { + run(paragraphId, true); + } + + /** + * Run paragraph by id + * @param paragraphId + * @param checkCurrentParagraph + */ + @ZeppelinApi + public void run(String paragraphId, boolean checkCurrentParagraph) { String noteId = interpreterContext.getNoteId(); - run(noteId, paragraphId, interpreterContext); + run(noteId, paragraphId, interpreterContext, checkCurrentParagraph); + } + + /** + * Run paragraph by id + * @param noteId + */ + @ZeppelinApi + public void run(String noteId, String paragraphId, InterpreterContext context) { + run(noteId, paragraphId, context, true); } /** @@ -331,8 +350,9 @@ public class ZeppelinContext { * @param context */ @ZeppelinApi - public void run(String noteId, String paragraphId, InterpreterContext context) { - if (paragraphId.equals(context.getParagraphId())) { + public void run(String noteId, String paragraphId, InterpreterContext context, + boolean checkCurrentParagraph) { + if (paragraphId.equals(context.getParagraphId()) && checkCurrentParagraph) { throw new InterpreterException("Can not run current Paragraph"); } @@ -412,24 +432,50 @@ public class ZeppelinContext { */ @ZeppelinApi public void run(int idx) { + run(idx, true); + } + + /** + * + * @param idx paragraph index + * @param checkCurrentParagraph check whether you call this run method in the current paragraph. + * Set it to false only when you are sure you are not invoking this method to run current + * paragraph. Otherwise you would run current paragraph in infinite loop. + */ + public void run(int idx, boolean checkCurrentParagraph) { String noteId = interpreterContext.getNoteId(); - run(noteId, idx, interpreterContext); + run(noteId, idx, interpreterContext, checkCurrentParagraph); } /** * Run paragraph at index + * @param noteId * @param idx index starting from 0 * @param context interpreter context */ public void run(String noteId, int idx, InterpreterContext context) { + run(noteId, idx, context, true); + } + + /** + * + * @param noteId + * @param idx paragraph index + * @param context interpreter context + * @param checkCurrentParagraph check whether you call this run method in the current paragraph. + * Set it to false only when you are sure you are not invoking this method to run current + * paragraph. Otherwise you would run current paragraph in infinite loop. + */ + public void run(String noteId, int idx, InterpreterContext context, + boolean checkCurrentParagraph) { List runners = getInterpreterContextRunner(noteId, context); if (idx >= runners.size()) { throw new InterpreterException("Index out of bound"); } InterpreterContextRunner runner = runners.get(idx); - if (runner.getParagraphId().equals(context.getParagraphId())) { - throw new InterpreterException("Can not run current Paragraph"); + if (runner.getParagraphId().equals(context.getParagraphId()) && checkCurrentParagraph) { + throw new InterpreterException("Can not run current Paragraph: " + runner.getParagraphId()); } runner.run(); diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObject.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObject.java index 929533bd1e..84ee67e7ff 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObject.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/AngularObject.java @@ -34,6 +34,8 @@ import org.slf4j.LoggerFactory; * @param */ public class AngularObject { + private static final Logger LOGGER = LoggerFactory.getLogger(AngularObject.class); + private String name; private T object; @@ -172,7 +174,7 @@ public class AngularObject { if (emit) { emit(); } - + LOGGER.debug("Update angular object: " + name + " with value: " + o); final Logger logger = LoggerFactory.getLogger(AngularObject.class); List ws = new LinkedList<>(); synchronized (watchers) { diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java index 23d80a41c7..87a6bcaa93 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java @@ -183,10 +183,11 @@ public class NotebookServer extends WebSocketServlet Notebook notebook = notebook(); try { Message messagereceived = deserializeMessage(msg); - LOG.debug("RECEIVE << " + messagereceived.op); - LOG.debug("RECEIVE PRINCIPAL << " + messagereceived.principal); - LOG.debug("RECEIVE TICKET << " + messagereceived.ticket); - LOG.debug("RECEIVE ROLES << " + messagereceived.roles); + LOG.debug("RECEIVE << " + messagereceived.op + + ", RECEIVE PRINCIPAL << " + messagereceived.principal + + ", RECEIVE TICKET << " + messagereceived.ticket + + ", RECEIVE ROLES << " + messagereceived.roles + + ", RECEIVE DATA << " + messagereceived.data); if (LOG.isTraceEnabled()) { LOG.trace("RECEIVE MSG = " + messagereceived); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java index 63c3ab1ffe..afdae10b66 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java @@ -137,7 +137,7 @@ public class ZeppelinIT extends AbstractZeppelinIT { * z.run(2, context) * } */ - setTextOfParagraph(4, "z.angularWatch(\"myVar\", (before:Object, after:Object, context:org.apache.zeppelin.interpreter.InterpreterContext)=>{ z.run(2)})"); + setTextOfParagraph(4, "z.angularWatch(\"myVar\", (before:Object, after:Object, context:org.apache.zeppelin.interpreter.InterpreterContext)=>{ z.run(2, false)})"); runParagraph(4); waitForParagraph(4, "FINISHED"); @@ -157,6 +157,21 @@ public class ZeppelinIT extends AbstractZeppelinIT { waitForText("myVar=3", By.xpath( getParagraphXPath(3) + "//div[contains(@id,\"_text\") and @class=\"text\"]")); + + /* + * Click element, again and see watcher still works + */ + driver.findElement(By.xpath( + getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]")).click(); + // check expected text + waitForText("BindingTest_4_", By.xpath( + getParagraphXPath(1) + "//div[@id=\"angularTestButton\"]")); + waitForParagraph(3, "FINISHED"); + + // check expected text by watcher + waitForText("myVar=4", By.xpath( + getParagraphXPath(3) + "//div[contains(@id,\"_text\") and @class=\"text\"]")); + /* * Unbind * z.angularUnbind("myVar") From 4d6485737cf72bc156c65c419b8e6e6cc907f3ea Mon Sep 17 00:00:00 2001 From: soralee Date: Tue, 14 Mar 2017 14:07:44 +0900 Subject: [PATCH 04/12] [ZEPPELIN-1357,1892,1370][Umbrella] Text overlap in the MultiBarChart ### What is this PR for? If using the `multiChartBar`, it often occurs overlap in text. So, this PR is for preventing to overlap in text. >~~The below is the case what I updated.~~ ~~1. The xLabel generally show all of the text without rotation (less than 30).~~ ~~2. If the xLabel size is over than 30 and less than 80, the xLabel text are rotated as 90 degree and displayed.~~ ~~3. If the xLabel size is over than 80, the xLabel text are disappeared, but the tooltip is displayed.~~ >~~I have made improvements based on my thinking, so I would appreciate to give me your feedback.~~ ~~And if merged this PR, I'll update the feature so that user can use `min` and `max` variables by user on web.~~ **[Update]** To prevent overlap in xLabel text, in one way which is talked the below mentions, user who is using zeppelin could select to visualization type of xLabel such as `rotate 45 degree` or `hide`. ### What type of PR is it? [Bug Fix | Improvement ] ### What is the Jira issue? * [ZEPPELIN-1357; UI - label wrapping not done properly in charts](https://issues.apache.org/jira/browse/ZEPPELIN-1357) * [ZEPPELIN-1892; Display label vertically or horizontally smartly](https://issues.apache.org/jira/browse/ZEPPELIN-1892) * [ZEPPELIN-1370; Label overlaps - in default visualization example barchart](https://issues.apache.org/jira/browse/ZEPPELIN-1370) ### How should this be tested? 1. Run the paragraph for bank data in the `Basic Features (Spark)` notebook. 2. Execute the following query. ``` %sql select * from bank ``` 3. Use the pivot in the `Setting` toggle in the `MultiBarChart` such as the screenshots. ### Screenshots (if appropriate) **[Before]** ![z_zeppelin-1357_b](https://cloud.githubusercontent.com/assets/8110458/23898303/f80d7bbc-08f3-11e7-9b24-3248c492b8af.png) ![z_1357_b3_](https://cloud.githubusercontent.com/assets/8110458/23898435/75b3ae60-08f4-11e7-9d7c-31746f0d4edc.png) **[After]** ![peek 2017-04-17 14-58](https://cloud.githubusercontent.com/assets/8110458/25080820/a3bfc1c0-2381-11e7-9c2b-16c0aa71234e.gif) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: soralee Closes #2133 from soralee/ZEPPELIN-1357_overlap_text and squashes the following commits: 32d7e37 [soralee] rebase master --- .../builtins/visualization-barchart.js | 89 ++++++++++++++++++- .../builtins/visualization-nvd3chart.js | 2 +- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-barchart.js b/zeppelin-web/src/app/visualization/builtins/visualization-barchart.js index ffc0c22fc7..15f03376f5 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-barchart.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-barchart.js @@ -45,6 +45,7 @@ export default class BarchartVisualization extends Nvd3ChartVisualization { true); super.render(d3Data); + this.config.changeXLabel(this.config.xLabelStatus); }; /** @@ -57,19 +58,99 @@ export default class BarchartVisualization extends Nvd3ChartVisualization { configureChart(chart) { var self = this; + var configObj = self.config; + chart.yAxis.axisLabelDistance(50); chart.yAxis.tickFormat(function(d) {return self.yAxisTickFormat(d);}); - this.chart.stacked(this.config.stacked); + self.chart.stacked(this.config.stacked); + + self.config.changeXLabel = function(type) { + switch (type) { + case 'default': + self.chart._options['showXAxis'] = true; + self.chart._options['margin'] = {bottom: 50}; + self.chart.xAxis.rotateLabels(0); + configObj.xLabelStatus = 'default'; + break; + case 'rotate': + self.chart._options['showXAxis'] = true; + self.chart._options['margin'] = {bottom: 140}; + self.chart.xAxis.rotateLabels(-45); + configObj.xLabelStatus = 'rotate'; + break; + case 'hide': + self.chart._options['showXAxis'] = false; + self.chart._options['margin'] = {bottom: 50}; + d3.select('#' + self.targetEl[0].id + '> svg').select('g.nv-axis.nv-x').selectAll('*').remove(); + configObj.xLabelStatus = 'hide'; + break; + } + }; + + self.config.isXLabelStatus = function(type) { + if (configObj.xLabelStatus === type) { + return true; + } else { + return false; + } + }; - var self = this; this.chart.dispatch.on('stateChange', function(s) { - self.config.stacked = s.stacked; + configObj.stacked = s.stacked; // give some time to animation finish setTimeout(function() { - self.emitConfig(self.config); + self.emitConfig(configObj); }, 500); }); }; + + + + getSetting(chart) { + var self = this; + var configObj = self.config; + + // default to visualize xLabel + if (typeof(configObj.xLabelStatus) === 'undefined') { + configObj.changeXLabel('default'); + } + + return { + template: `
+ xAxis : +
+ +
+ + + + + +
`, + scope: { + config: configObj, + save: function(type) { + configObj.changeXLabel(type); + self.emitConfig(configObj); + } + } + }; + }; } diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-nvd3chart.js b/zeppelin-web/src/app/visualization/builtins/visualization-nvd3chart.js index b0f569e26d..930e435295 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-nvd3chart.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-nvd3chart.js @@ -44,7 +44,7 @@ export default class Nvd3ChartVisualization extends Visualization { var height = this.targetEl.height(); // turn off animation when dataset is too large. (for performance issue) - // still, since dataset is large, the chart content sequentially appears like animated. + // still, since dataset is large, the chart content sequentially appears like animated try { if (d3g[0].values.length > numberOfDataThreshold) { animationDuration = 0; From c3fbb1ef664e7a54a8bc52eee435b91be8a6e16d Mon Sep 17 00:00:00 2001 From: soralee Date: Tue, 11 Apr 2017 09:43:03 +0900 Subject: [PATCH 05/12] [MINOR] fix typo from 'interpeter' to 'interpreter' ### What is this PR for? Fix typo from `interpeter` to `interpreter` ### What type of PR is it? [Improvement] ### What is the Jira issue? * No, it's minor issue ### How should this be tested? - N/A ### Screenshots (if appropriate) - N/A ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Author: soralee Closes #2244 from soralee/typo_interpreter and squashes the following commits: 863dc4f [soralee] fix typo --- zeppelin-web/src/app/notebook/notebook.controller.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 5fde0f49c4..132241996f 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -712,13 +712,13 @@ function NotebookCtrl($scope, $route, $routeParams, $location, $rootScope, angular.element('.permissionsForm select').find('option:not([is-select2="false"])').remove(); } - $scope.restartInterpreter = function(interpeter) { + $scope.restartInterpreter = function(interpreter) { var thisConfirm = BootstrapDialog.confirm({ closable: false, closeByBackdrop: false, closeByKeyboard: false, title: '', - message: 'Do you want to restart ' + interpeter.name + ' interpreter?', + message: 'Do you want to restart ' + interpreter.name + ' interpreter?', callback: function(result) { if (result) { var payload = { @@ -729,9 +729,9 @@ function NotebookCtrl($scope, $route, $routeParams, $location, $rootScope, thisConfirm.$modalFooter.find('button:contains("OK")') .html(' Saving Setting'); - $http.put(baseUrlSrv.getRestApiBase() + '/interpreter/setting/restart/' + interpeter.id, payload) + $http.put(baseUrlSrv.getRestApiBase() + '/interpreter/setting/restart/' + interpreter.id, payload) .success(function(data, status, headers, config) { - var index = _.findIndex($scope.interpreterSettings, {'id': interpeter.id}); + var index = _.findIndex($scope.interpreterSettings, {'id': interpreter.id}); $scope.interpreterSettings[index] = data.body; thisConfirm.close(); }).error(function(data, status, headers, config) { From 4b1b521fc34fd3c841e38ab591af0bc7df21a64e Mon Sep 17 00:00:00 2001 From: 1ambda <1amb4a@gmail.com> Date: Sat, 15 Apr 2017 17:18:56 +0900 Subject: [PATCH 06/12] [ZEPPELIN-2179] Clear output DOES NOT work in personalized mode (master) ### What is this PR for? `clear output` (`cmd` + `opt` + `L`) doesn't work in the personalized mode. ### What type of PR is it? [Bug Fix] ### Todos NONE ### What is the Jira issue? [ZEPPELIN-2179](https://issues.apache.org/jira/browse/ZEPPELIN-2179) ### How should this be tested? 1. Configure shiro 2. Start Zeppelin and login in 2 browsers (e.g chrome and safari) with 2 different users (**DO NOT USE `dev` mode: localhost:9000**) 3. Create a note 4. Enable personalized mode 5. Run paragraph and clear output in each browser ### Screenshots (if appropriate) NONE ### Questions: * Does the licenses files need update? - NO * Is there breaking changes for older versions? - NO * Does this needs documentation? - NO Author: 1ambda <1amb4a@gmail.com> Closes #2253 from 1ambda/ZEPPELIN-2179/clear-output-doesnt-work-in-person-mode-for-master and squashes the following commits: eeff440ec [1ambda] fix: Clear personalized output before running b7387849f [1ambda] fix: Clear output in personalized paragraph --- .../zeppelin/socket/NotebookServer.java | 39 +++++++++++++++++-- .../org/apache/zeppelin/notebook/Note.java | 29 ++++++++++++-- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java index 87a6bcaa93..1aa4f2893c 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java @@ -644,6 +644,22 @@ public class NotebookServer extends WebSocketServlet broadcast(noteId, new Message(OP.INTERPRETER_BINDINGS).put("interpreterBindings", settingList)); } + public void unicastParagraph(Note note, Paragraph p, String user) { + if (!note.isPersonalizedMode() || p == null || user == null) { + return; + } + + if (!userConnectedSockets.containsKey(user)) { + LOG.warn("Failed to send unicast. user {} that is not in connections map", user); + return; + } + + for (NotebookSocket conn : userConnectedSockets.get(user)) { + Message m = new Message(OP.PARAGRAPH).put("paragraph", p); + unicast(m, conn); + } + } + public void broadcastParagraph(Note note, Paragraph p) { if (note.isPersonalizedMode()) { broadcastParagraphs(p.getUserParagraphMap(), p); @@ -1301,9 +1317,15 @@ public class NotebookServer extends WebSocketServlet } final Note note = notebook.getNote(noteId); - note.clearParagraphOutput(paragraphId); - Paragraph paragraph = note.getParagraph(paragraphId); - broadcastParagraph(note, paragraph); + if (note.isPersonalizedMode()) { + String user = fromMessage.principal; + Paragraph p = note.clearPersonalizedParagraphOutput(paragraphId, user); + unicastParagraph(note, p, user); + } else { + note.clearParagraphOutput(paragraphId); + Paragraph paragraph = note.getParagraph(paragraphId); + broadcastParagraph(note, paragraph); + } } private void completion(NotebookSocket conn, HashSet userAndRoles, Notebook notebook, @@ -1696,12 +1718,21 @@ public class NotebookServer extends WebSocketServlet return; } + // 1. clear paragraph only if personalized, + // otherwise this will be handed in `onOutputClear` + final Note note = notebook.getNote(noteId); + if (note.isPersonalizedMode()) { + String user = fromMessage.principal; + Paragraph p = note.clearPersonalizedParagraphOutput(paragraphId, user); + unicastParagraph(note, p, user); + } + + // 2. set paragraph values String text = (String) fromMessage.get("paragraph"); String title = (String) fromMessage.get("title"); Map params = (Map) fromMessage.get("params"); Map config = (Map) fromMessage.get("config"); - final Note note = notebook.getNote(noteId); Paragraph p = setParagraphUsingMessage(note, fromMessage, paragraphId, text, title, params, config); diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java index dd2c0946f2..3e6ab23a1e 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java @@ -390,6 +390,26 @@ public class Note implements Serializable, ParagraphJobListener { return null; } + public void clearParagraphOutputFields(Paragraph p) { + p.setReturn(null, null); + p.clearRuntimeInfo(null); + } + + public Paragraph clearPersonalizedParagraphOutput(String paragraphId, String user) { + synchronized (paragraphs) { + for (Paragraph p : paragraphs) { + if (!p.getId().equals(paragraphId)) { + continue; + } + + p = p.getUserParagraphMap().get(user); + clearParagraphOutputFields(p); + return p; + } + } + return null; + } + /** * Clear paragraph output by id. * @@ -399,11 +419,12 @@ public class Note implements Serializable, ParagraphJobListener { public Paragraph clearParagraphOutput(String paragraphId) { synchronized (paragraphs) { for (Paragraph p : paragraphs) { - if (p.getId().equals(paragraphId)) { - p.setReturn(null, null); - p.clearRuntimeInfo(null); - return p; + if (!p.getId().equals(paragraphId)) { + continue; } + + clearParagraphOutputFields(p); + return p; } } return null; From 7b585c7399f29492c308f83e342cac29b0c7ca07 Mon Sep 17 00:00:00 2001 From: Jeff Zhang Date: Wed, 12 Apr 2017 10:50:16 +0800 Subject: [PATCH 07/12] ZEPPELIN-2395. Refactor Input.java to make dynamic forms extensible ### What is this PR for? Currently, zeppelin only support 3 kinds of dynamic form controls: TextBox, Select, CheckBox. All the things are in `Input.java`, this is hard to add new controls, this PR is for refactoring Input to make dynamic forms extensible. Main Changes: * Make `Input` as the base class of dynamic forms also use it as the factory class * All the concret dynamic forms extend `Input` * Add method `toJson` and `fromJson` for `GUI` for `GUI`'s serialization/deserialization. I plan to do it for other classes as well, so that we can remove duplicated serde code and also make it easy to test serialization/deserialization * Change `z.input` to `z.textbox` as I think z.input is a little misleading. But I still keep `z.input` and make `z.input` as deprecated. * Ideally the new input forms' json should be the same as the old input form json. But there's one bug in the old input form, `type` is missing if the input forms are created in frontend for textbox and select. So I keep the old input forms for compatibility. I will load the old input forms json and convert it into new input forms, and after saving, `note.json` would have the new input forms json. After this PR, user needs to do 3 things to add new ui controls * Implement its UI control classes, (refer TextBox/CheckBox/Select), and specify it in `TypeAdapterFactory` of `Input` for serde. * Add parsing logic in `Input.getInputForm` if you want to support this control in frontend. * Add display logic in `paragraph-parameterizedQueryForm.html` ### What type of PR is it? [ Improvement | Refactoring] ### Todos * [ ] - Task ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-2395 ### How should this be tested? Test is added ### Screenshots (if appropriate) ### Questions: * Does the licenses files need update? Yes * Is there breaking changes for older versions? No * Does this needs documentation? No Author: Jeff Zhang Closes #2245 from zjffdu/ZEPPELIN-2395 and squashes the following commits: 16d42a8 [Jeff Zhang] ZEPPELIN-2395. Refactor Input.java to make dynamic forms extensible --- LICENSE | 1 + .../zeppelin/cassandra/InterpreterLogic.scala | 2 +- .../cassandra/InterpreterLogicTest.java | 2 +- .../org/apache/zeppelin/groovy/GObject.java | 2 +- pom.xml | 7 + .../main/resources/python/zeppelin_python.py | 2 +- .../zeppelin/spark/ZeppelinContext.java | 27 ++- zeppelin-interpreter/pom.xml | 5 + .../java/org/apache/zeppelin/display/GUI.java | 69 ++++++- .../org/apache/zeppelin/display/Input.java | 172 +++++++----------- .../org/apache/zeppelin/display/OldInput.java | 87 +++++++++ .../display/RuntimeTypeAdapterFactory.java | 149 +++++++++++++++ .../apache/zeppelin/display/ui/CheckBox.java | 45 +++++ .../zeppelin/display/ui/OptionInput.java | 85 +++++++++ .../apache/zeppelin/display/ui/Select.java | 36 ++++ .../apache/zeppelin/display/ui/TextBox.java | 38 ++++ .../interpreter/remote/RemoteInterpreter.java | 4 +- .../remote/RemoteInterpreterServer.java | 4 +- .../org/apache/zeppelin/display/GUITest.java | 120 ++++++++++++ .../apache/zeppelin/display/InputTest.java | 35 ++-- .../zeppelin/socket/NotebookServer.java | 5 +- .../integration/ParagraphActionsIT.java | 2 +- .../rest/ZeppelinSparkClusterTest.java | 2 +- .../paragraph-parameterizedQueryForm.html | 20 +- .../websocketEvents.factory.js | 1 + .../org/apache/zeppelin/notebook/Note.java | 19 ++ .../notebook/repo/AzureNotebookRepo.java | 2 +- .../notebook/repo/S3NotebookRepo.java | 2 +- .../notebook/repo/VFSNotebookRepo.java | 2 +- 29 files changed, 791 insertions(+), 156 deletions(-) create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/OldInput.java create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/RuntimeTypeAdapterFactory.java create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/CheckBox.java create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/OptionInput.java create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/Select.java create mode 100644 zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/TextBox.java create mode 100644 zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/GUITest.java diff --git a/LICENSE b/LICENSE index 83a913190a..e206a6c2eb 100644 --- a/LICENSE +++ b/LICENSE @@ -255,6 +255,7 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (Apache 2.0) Bootstrap v3.0.2 (http://getbootstrap.com/) - https://github.com/twbs/bootstrap/blob/v3.0.2/LICENSE (Apache 2.0) Software under ./bigquery/* was developed at Google (http://www.google.com/). Licensed under the Apache v2.0 License. (Apache 2.0) Roboto Font (https://github.com/google/roboto/) + (Apache 2.0) Gson extra (https://github.com/DanySK/gson-extras) ======================================================================== BSD 3-Clause licenses diff --git a/cassandra/src/main/scala/org/apache/zeppelin/cassandra/InterpreterLogic.scala b/cassandra/src/main/scala/org/apache/zeppelin/cassandra/InterpreterLogic.scala index 363da7b1d2..c83a186a9c 100644 --- a/cassandra/src/main/scala/org/apache/zeppelin/cassandra/InterpreterLogic.scala +++ b/cassandra/src/main/scala/org/apache/zeppelin/cassandra/InterpreterLogic.scala @@ -30,7 +30,7 @@ import com.datastax.driver.core.exceptions.DriverException import com.datastax.driver.core.policies.{LoggingRetryPolicy, FallthroughRetryPolicy, DowngradingConsistencyRetryPolicy, Policies} import org.apache.zeppelin.cassandra.TextBlockHierarchy._ import org.apache.zeppelin.display.AngularObjectRegistry -import org.apache.zeppelin.display.Input.ParamOption +import org.apache.zeppelin.display.ui.OptionInput.ParamOption import org.apache.zeppelin.interpreter.InterpreterResult.Code import org.apache.zeppelin.interpreter.{InterpreterException, InterpreterResult, InterpreterContext} import org.slf4j.LoggerFactory diff --git a/cassandra/src/test/java/org/apache/zeppelin/cassandra/InterpreterLogicTest.java b/cassandra/src/test/java/org/apache/zeppelin/cassandra/InterpreterLogicTest.java index 698397ae94..f3848fd1a7 100644 --- a/cassandra/src/test/java/org/apache/zeppelin/cassandra/InterpreterLogicTest.java +++ b/cassandra/src/test/java/org/apache/zeppelin/cassandra/InterpreterLogicTest.java @@ -34,7 +34,7 @@ import com.datastax.driver.core.Statement; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.display.Input.ParamOption; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterException; import org.junit.Rule; diff --git a/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java b/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java index e46065161b..7f6809a899 100644 --- a/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java +++ b/groovy/src/main/java/org/apache/zeppelin/groovy/GObject.java @@ -36,7 +36,7 @@ import org.apache.zeppelin.interpreter.InterpreterContextRunner; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.GUI; -import org.apache.zeppelin.display.Input.ParamOption; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; import org.apache.zeppelin.annotation.ZeppelinApi; import org.apache.zeppelin.interpreter.RemoteWorksController; import org.apache.zeppelin.interpreter.InterpreterException; diff --git a/pom.xml b/pom.xml index e1a2094d44..3bede62938 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,7 @@ 1.2.17 0.9.2 2.2 + 0.2.1 15.0 9.2.15.v20160210 4.3.3 @@ -192,6 +193,12 @@ ${gson.version} + + org.danilopianini + gson-extras + ${gson-extras.version} + + commons-configuration commons-configuration diff --git a/python/src/main/resources/python/zeppelin_python.py b/python/src/main/resources/python/zeppelin_python.py index 31b993dec2..eff88249ee 100644 --- a/python/src/main/resources/python/zeppelin_python.py +++ b/python/src/main/resources/python/zeppelin_python.py @@ -53,7 +53,7 @@ class PyZeppelinContext(object): def __init__(self, z): self.z = z - self.paramOption = gateway.jvm.org.apache.zeppelin.display.Input.ParamOption + self.paramOption = gateway.jvm.org.apache.zeppelin.display.ui.OptionInput.ParamOption self.javaList = gateway.jvm.java.util.ArrayList self.max_result = 1000 self._displayhook = lambda *args: None 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 7e1ab704b3..b78410f9f1 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java +++ b/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java @@ -40,7 +40,7 @@ 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.Input.ParamOption; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; import org.apache.zeppelin.interpreter.InterpreterContext; import org.apache.zeppelin.interpreter.InterpreterContextRunner; import org.apache.zeppelin.interpreter.InterpreterException; @@ -114,14 +114,33 @@ public class ZeppelinContext { public SQLContext sqlContext; private GUI gui; + /** + * @deprecated use z.textbox instead + * + */ + @Deprecated @ZeppelinApi public Object input(String name) { - return input(name, ""); + return textbox(name); + } + + /** + * @deprecated use z.textbox instead + */ + @Deprecated + @ZeppelinApi + public Object input(String name, Object defaultValue) { + return textbox(name, defaultValue.toString()); } @ZeppelinApi - public Object input(String name, Object defaultValue) { - return gui.input(name, defaultValue); + public Object textbox(String name) { + return textbox(name, ""); + } + + @ZeppelinApi + public Object textbox(String name, String defaultValue) { + return gui.textbox(name, defaultValue); } @ZeppelinApi diff --git a/zeppelin-interpreter/pom.xml b/zeppelin-interpreter/pom.xml index e55144cebb..109099cfc7 100644 --- a/zeppelin-interpreter/pom.xml +++ b/zeppelin-interpreter/pom.xml @@ -61,6 +61,11 @@ gson + + org.danilopianini + gson-extras + + org.apache.commons commons-exec diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java index 40ce8caeaf..66b21c62ef 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java @@ -17,17 +17,27 @@ package org.apache.zeppelin.display; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.zeppelin.display.ui.CheckBox; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; +import org.apache.zeppelin.display.ui.Select; +import org.apache.zeppelin.display.ui.TextBox; + import java.io.Serializable; import java.util.*; -import org.apache.zeppelin.display.Input.ParamOption; /** * Settings of a form. */ public class GUI implements Serializable { + private static Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(Input.TypeAdapterFactory) + .create(); + Map params = new HashMap<>(); // form parameters from client LinkedHashMap forms = new LinkedHashMap<>(); // form configuration @@ -51,19 +61,29 @@ public class GUI implements Serializable { this.forms = forms; } + @Deprecated + public Object input(String id) { + return textbox(id, ""); + } + + @Deprecated public Object input(String id, Object defaultValue) { + return textbox(id, defaultValue.toString()); + } + + public Object textbox(String id, String defaultValue) { // first find values from client and then use default Object value = params.get(id); if (value == null) { value = defaultValue; } - forms.put(id, new Input(id, defaultValue, "input")); + forms.put(id, new TextBox(id, defaultValue)); return value; } - public Object input(String id) { - return input(id, ""); + public Object textbox(String id) { + return textbox(id, ""); } public Object select(String id, Object defaultValue, ParamOption[] options) { @@ -71,7 +91,7 @@ public class GUI implements Serializable { if (value == null) { value = defaultValue; } - forms.put(id, new Input(id, defaultValue, "select", options)); + forms.put(id, new Select(id, defaultValue, options)); return value; } @@ -81,7 +101,7 @@ public class GUI implements Serializable { if (checked == null) { checked = defaultChecked; } - forms.put(id, new Input(id, defaultChecked, "checkbox", options)); + forms.put(id, new CheckBox(id, defaultChecked, options)); List filtered = new LinkedList<>(); for (Object o : checked) { if (isValidOption(o, options)) { @@ -103,4 +123,41 @@ public class GUI implements Serializable { public void clear() { this.forms = new LinkedHashMap<>(); } + + public String toJson() { + return gson.toJson(this); + } + + public void convertOldInput() { + for (Map.Entry entry : forms.entrySet()) { + if (entry.getValue() instanceof OldInput) { + Input convertedInput = convertFromOldInput((OldInput) entry.getValue()); + forms.put(entry.getKey(), convertedInput); + } + } + } + + public static GUI fromJson(String json) { + GUI gui = gson.fromJson(json, GUI.class); + gui.convertOldInput(); + return gui; + } + + private Input convertFromOldInput(OldInput oldInput) { + Input convertedInput = null; + + if (oldInput.options == null || oldInput instanceof OldInput.OldTextBox) { + convertedInput = new TextBox(oldInput.name, oldInput.defaultValue.toString()); + } else if (oldInput instanceof OldInput.OldCheckBox) { + convertedInput = new CheckBox(oldInput.name, (List) oldInput.defaultValue, oldInput.options); + } else if (oldInput instanceof OldInput && oldInput.options != null) { + convertedInput = new Select(oldInput.name, oldInput.defaultValue, oldInput.options); + } else { + throw new RuntimeException("Can not convert this OldInput."); + } + convertedInput.setDisplayName(oldInput.getDisplayName()); + convertedInput.setHidden(oldInput.isHidden()); + convertedInput.setArgument(oldInput.getArgument()); + return convertedInput; + } } diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/Input.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/Input.java index 4924b2ba71..12fa7820d6 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/Input.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/Input.java @@ -18,6 +18,8 @@ package org.apache.zeppelin.display; import org.apache.commons.lang.StringUtils; +import org.apache.zeppelin.display.ui.*; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; import java.io.Serializable; import java.util.*; @@ -25,105 +27,43 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * Input type. + * Base class for dynamic forms. Also used as factory class of dynamic forms. + * + * @param */ -public class Input implements Serializable { - /** - * Parameters option. - */ - public static class ParamOption { - Object value; - String displayName; +public class Input implements Serializable { - public ParamOption(Object value, String displayName) { - super(); - this.value = value; - this.displayName = displayName; - } + // @TODO(zjffdu). Use gson's RuntimeTypeAdapterFactory and remove the old input form support + // in future. + public static final RuntimeTypeAdapterFactory TypeAdapterFactory = + RuntimeTypeAdapterFactory.of(Input.class, "type") + .registerSubtype(TextBox.class, "TextBox") + .registerSubtype(Select.class, "Select") + .registerSubtype(CheckBox.class, "CheckBox") + .registerSubtype(OldInput.OldTextBox.class, "input") + .registerSubtype(OldInput.OldSelect.class, "select") + .registerSubtype(OldInput.OldCheckBox.class, "checkbox") + .registerSubtype(OldInput.class, null); - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ParamOption that = (ParamOption) o; - - if (value != null ? !value.equals(that.value) : that.value != null) return false; - return displayName != null ? displayName.equals(that.displayName) : that.displayName == null; - - } - - @Override - public int hashCode() { - int result = value != null ? value.hashCode() : 0; - result = 31 * result + (displayName != null ? displayName.hashCode() : 0); - return result; - } - - public Object getValue() { - return value; - } - - public void setValue(Object value) { - this.value = value; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } + protected String name; + protected String displayName; + protected T defaultValue; + protected boolean hidden; + protected String argument; + public Input() { } - String name; - String displayName; - String type; - String argument; - Object defaultValue; - ParamOption[] options; - boolean hidden; - - public Input(String name, Object defaultValue, String type) { - this.name = name; - this.displayName = name; - this.defaultValue = defaultValue; - this.type = type; - } - - public Input(String name, Object defaultValue, String type, ParamOption[] options) { - this.name = name; - this.displayName = name; - this.defaultValue = defaultValue; - this.type = type; - this.options = options; - } - - public Input(String name, String displayName, String type, String argument, Object defaultValue, - ParamOption[] options, boolean hidden) { - super(); - this.name = name; - this.displayName = displayName; - this.argument = argument; - this.type = type; - this.defaultValue = defaultValue; - this.options = options; - this.hidden = hidden; - } - - @Override - public boolean equals(Object o) { - return name.equals(((Input) o).getName()); + public boolean isHidden() { + return hidden; } public String getName() { - return name; + return this.name; } - public void setName(String name) { - this.name = name; + public T getDefaultValue() { + return defaultValue; } public String getDisplayName() { @@ -134,41 +74,37 @@ public class Input implements Serializable { this.displayName = displayName; } - public String getType() { - return type; + public void setArgument(String argument) { + this.argument = argument; } - public void setType(String type) { - this.type = type; + public void setHidden(boolean hidden) { + this.hidden = hidden; } - public Object getDefaultValue() { - return defaultValue; + public String getArgument() { + return argument; } - public void setDefaultValue(Object defaultValue) { - this.defaultValue = defaultValue; + public static TextBox textbox(String name, String defaultValue) { + return new TextBox(name, defaultValue); } - public ParamOption[] getOptions() { - return options; + public static Select select(String name, Object defaultValue, ParamOption[] options) { + return new Select(name, defaultValue, options); } - public void setOptions(ParamOption[] options) { - this.options = options; - } - - public boolean isHidden() { - return hidden; + public static CheckBox checkbox(String name, Object[] defaultChecked, ParamOption[] options) { + return new CheckBox(name, defaultChecked, options); } // Syntax of variables: ${TYPE:NAME=DEFAULT_VALUE1|DEFAULT_VALUE2|...,VALUE1|VALUE2|...} // Type is optional. Type may contain an optional argument with syntax: TYPE(ARG) // NAME and VALUEs may contain an optional display name with syntax: NAME(DISPLAY_NAME) // DEFAULT_VALUEs may not contain display name - // Examples: ${age} input form without default value - // ${age=3} input form with default value - // ${age(Age)=3} input form with display name and default value + // Examples: ${age} textbox form without default value + // ${age=3} textbox form with default value + // ${age(Age)=3} textbox form with display name and default value // ${country=US(United States)|UK|JP} select form with // ${checkbox( or ):country(Country)=US|JP,US(United States)|UK|JP} // checkbox form with " or " as delimiter: will be @@ -282,7 +218,22 @@ public class Input implements Serializable { } - return new Input(varName, displayName, type, arg, defaultValue, paramOptions, hidden); + Input input = null; + if (type == null) { + if (paramOptions == null) { + input = new TextBox(varName, (String) defaultValue); + } else { + input = new Select(varName, defaultValue, paramOptions); + } + } else if (type.equals("checkbox")) { + input = new CheckBox(varName, (Object[]) defaultValue, paramOptions); + } else { + throw new RuntimeException("Could not recognize dynamic form with type: " + type); + } + input.setArgument(arg); + input.setDisplayName(displayName); + input.setHidden(hidden); + return input; } public static LinkedHashMap extractSimpleQueryForm(String script) { @@ -314,11 +265,12 @@ public class Input implements Serializable { if (params.containsKey(input.name)) { value = params.get(input.name); } else { - value = input.defaultValue; + value = input.getDefaultValue(); } String expanded; if (value instanceof Object[] || value instanceof Collection) { // multi-selection + OptionInput optionInput = (OptionInput) input; String delimiter = input.argument; if (delimiter == null) { delimiter = DEFAULT_DELIMITER; @@ -327,7 +279,7 @@ public class Input implements Serializable { : Arrays.asList((Object[]) value); List validChecked = new LinkedList<>(); for (Object o : checked) { // filter out obsolete checked values - for (ParamOption option : input.getOptions()) { + for (ParamOption option : optionInput.getOptions()) { if (option.getValue().equals(o)) { validChecked.add(o); break; diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/OldInput.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/OldInput.java new file mode 100644 index 0000000000..7c67dad834 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/OldInput.java @@ -0,0 +1,87 @@ +/* + * 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 org.apache.zeppelin.display.ui.OptionInput.ParamOption; + +/** + * Old Input type. + * The reason I still keep Old Input is for compatibility. There's one bug in the old input forms. + * There's 2 ways to create input forms: frontend & backend. + * The bug is in frontend. The type would not be set correctly when input form + * is created in frontend (Input.getInputForm). + */ +public class OldInput extends Input { + + ParamOption[] options; + + public OldInput() {} + + public OldInput(String name, Object defaultValue) { + this.name = name; + this.displayName = name; + this.defaultValue = defaultValue; + } + + public OldInput(String name, Object defaultValue, ParamOption[] options) { + this.name = name; + this.displayName = name; + this.defaultValue = defaultValue; + this.options = options; + } + + @Override + public boolean equals(Object o) { + return name.equals(((OldInput) o).getName()); + } + + public ParamOption[] getOptions() { + return options; + } + + public void setOptions(ParamOption[] options) { + this.options = options; + } + + /** + * + */ + public static class OldTextBox extends OldInput { + public OldTextBox(String name, Object defaultValue) { + super(name, defaultValue); + } + } + + /** + * + */ + public static class OldSelect extends OldInput { + public OldSelect(String name, Object defaultValue, ParamOption[] options) { + super(name, defaultValue, options); + } + } + + /** + * + */ + public static class OldCheckBox extends OldInput { + public OldCheckBox(String name, Object defaultValue, ParamOption[] options) { + super(name, defaultValue, options); + } + } +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/RuntimeTypeAdapterFactory.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/RuntimeTypeAdapterFactory.java new file mode 100644 index 0000000000..da05caa633 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/RuntimeTypeAdapterFactory.java @@ -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 + * + * 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.*; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Copied from gson with minor changes to support old input forms + * + * @param + */ +public class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap>(); + private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); + + private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory(baseType, "type"); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map> labelToDelegate = new LinkedHashMap>(); + final Map, TypeAdapter> subtypeToDelegate = + new LinkedHashMap, TypeAdapter>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + String label = (labelJsonElement == null ? null : labelJsonElement.getAsString()); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + if (jsonObject.has(typeFieldName) && !srcType.getSimpleName().equals("OldInput")) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + JsonObject clone = new JsonObject(); + if (!srcType.getSimpleName().equals("OldInput")) { + clone.add(typeFieldName, new JsonPrimitive(label)); + } + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + } + }.nullSafe(); + } +} + diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/CheckBox.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/CheckBox.java new file mode 100644 index 0000000000..f9b4650f4d --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/CheckBox.java @@ -0,0 +1,45 @@ +/* + * 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.ui; + +import java.awt.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +/** + * Html Checkbox + */ +public class CheckBox extends OptionInput { + + public CheckBox() { + } + + public CheckBox(String name, Object[] defaultValue, ParamOption[] options) { + this.name = name; + this.displayName = name; + this.defaultValue = defaultValue; + this.options = options; + } + + public CheckBox(String name, Collection defaultValue, ParamOption[] options) { + this(name, defaultValue.toArray(), options); + } + +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/OptionInput.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/OptionInput.java new file mode 100644 index 0000000000..d5a1c0d2c1 --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/OptionInput.java @@ -0,0 +1,85 @@ +/* + * 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.ui; + +import org.apache.zeppelin.display.Input; + +/** + * Base class for Input with options + * + * @param + */ +public abstract class OptionInput extends Input { + + /** + * Parameters option. + */ + public static class ParamOption { + Object value; + String displayName; + + public ParamOption(Object value, String displayName) { + super(); + this.value = value; + this.displayName = displayName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ParamOption that = (ParamOption) o; + + if (value != null ? !value.equals(that.value) : that.value != null) return false; + return displayName != null ? displayName.equals(that.displayName) : that.displayName == null; + + } + + @Override + public int hashCode() { + int result = value != null ? value.hashCode() : 0; + result = 31 * result + (displayName != null ? displayName.hashCode() : 0); + return result; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + } + + protected ParamOption[] options; + + public ParamOption[] getOptions() { + return options; + } +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/Select.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/Select.java new file mode 100644 index 0000000000..212d3d747d --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/Select.java @@ -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.display.ui; + +/** + * Html Dropdown list + */ +public class Select extends OptionInput { + + public Select() { + + } + + public Select(String name, Object defaultValue, ParamOption[] options) { + this.name = name; + this.displayName = name; + this.defaultValue = defaultValue; + this.options = options; + } + +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/TextBox.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/TextBox.java new file mode 100644 index 0000000000..b9f994646d --- /dev/null +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/ui/TextBox.java @@ -0,0 +1,38 @@ +/* + * 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.ui; + +import org.apache.zeppelin.display.Input; + +/** + * Html TextBox control + */ +public class TextBox extends Input { + + public TextBox() { + + } + + public TextBox(String name, String defaultValue) { + this.name = name; + this.displayName = name; + this.defaultValue = defaultValue; + } + +} diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java index 2f9d2bbba5..123ad75741 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreter.java @@ -381,14 +381,14 @@ public class RemoteInterpreter extends Interpreter { context.getConfig().putAll(remoteConfig); if (form == FormType.NATIVE) { - GUI remoteGui = gson.fromJson(remoteResult.getGui(), GUI.class); + GUI remoteGui = GUI.fromJson(remoteResult.getGui()); currentGUI.clear(); currentGUI.setParams(remoteGui.getParams()); currentGUI.setForms(remoteGui.getForms()); } else if (form == FormType.SIMPLE) { final Map currentForms = currentGUI.getForms(); final Map currentParams = currentGUI.getParams(); - final GUI remoteGUI = gson.fromJson(remoteResult.getGui(), GUI.class); + final GUI remoteGUI = GUI.fromJson(remoteResult.getGui()); final Map remoteForms = remoteGUI.getForms(); final Map remoteParams = remoteGUI.getParams(); currentForms.putAll(remoteForms); diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java index 3b7ec5caa7..6c43813d93 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/remote/RemoteInterpreterServer.java @@ -592,7 +592,7 @@ public class RemoteInterpreterServer gson.fromJson(ric.getAuthenticationInfo(), AuthenticationInfo.class), (Map) gson.fromJson(ric.getConfig(), new TypeToken>() {}.getType()), - gson.fromJson(ric.getGui(), GUI.class), + GUI.fromJson(ric.getGui()), interpreterGroup.getAngularObjectRegistry(), interpreterGroup.getResourcePool(), contextRunners, output, remoteWorksController, eventClient); @@ -737,7 +737,7 @@ public class RemoteInterpreterServer result.code().name(), msg, gson.toJson(config), - gson.toJson(gui)); + gui.toJson()); } @Override diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/GUITest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/GUITest.java new file mode 100644 index 0000000000..6def2e7445 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/GUITest.java @@ -0,0 +1,120 @@ +/* + * 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 org.apache.commons.io.IOUtils; +import org.apache.zeppelin.display.ui.CheckBox; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; +import org.apache.zeppelin.display.ui.Select; +import org.apache.zeppelin.display.ui.TextBox; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class GUITest { + + private ParamOption[] options = new ParamOption[]{ + new ParamOption("1", "value_1"), + new ParamOption("2", "value_2") + }; + + private List checkedItems; + + @Before + public void setUp() { + checkedItems = new ArrayList<>(); + checkedItems.add("1"); + } + + @Test + public void testGson() { + GUI gui = new GUI(); + gui.textbox("textbox_1", "default_text_1"); + gui.select("select_1", "1", options); + List list = new ArrayList(); + list.add("1"); + gui.checkbox("checkbox_1", list, options); + + String json = gui.toJson(); + System.out.println(json); + GUI gui2 = GUI.fromJson(json); + assertEquals(gui2.toJson(), json); + assertEquals(gui2.forms, gui2.forms); + assertEquals(gui2.params, gui2.params); + } + + // Case 1. Old input forms are created in backend, in this case type is always set + @Test + public void testOldGson_1() throws IOException { + + GUI gui = new GUI(); + gui.forms.put("textbox_1", new OldInput.OldTextBox("textbox_1", "default_text_1")); + gui.forms.put("select_1", new OldInput.OldSelect("select_1", "1", options)); + gui.forms.put("checkbox_1", + new OldInput.OldCheckBox("checkbox_1", checkedItems, options)); + + // convert to old json format. + String json = gui.toJson(); + + // convert to new input forms + GUI gui2 = GUI.fromJson(json); + assertTrue(3 == gui2.forms.size()); + assertTrue(gui2.forms.get("textbox_1") instanceof TextBox); + assertEquals("default_text_1", gui2.forms.get("textbox_1").getDefaultValue()); + assertTrue(gui2.forms.get("select_1") instanceof Select); + assertEquals(options, ((Select) gui2.forms.get("select_1")).getOptions()); + assertTrue(gui2.forms.get("checkbox_1") instanceof CheckBox); + assertEquals(options, ((CheckBox) gui2.forms.get("checkbox_1")).getOptions()); + } + + // Case 2. Old input forms are created in frontend, in this case type is only set for checkbox + // Actually this is a bug due to method Input#getInputForm + @Test + public void testOldGson_2() throws IOException { + + GUI gui = new GUI(); + gui.forms.put("textbox_1", new OldInput("textbox_1", "default_text_1")); + gui.forms.put("select_1", new OldInput("select_1", "1", options)); + gui.forms.put("checkbox_1", + new OldInput.OldCheckBox("checkbox_1", checkedItems, options)); + + // convert to old json format. + String json = gui.toJson(); + + // convert to new input forms + GUI gui2 = GUI.fromJson(json); + assertTrue(3 == gui2.forms.size()); + assertTrue(gui2.forms.get("textbox_1") instanceof TextBox); + assertEquals("default_text_1", gui2.forms.get("textbox_1").getDefaultValue()); + assertTrue(gui2.forms.get("select_1") instanceof Select); + assertEquals(options, ((Select) gui2.forms.get("select_1")).getOptions()); + assertTrue(gui2.forms.get("checkbox_1") instanceof CheckBox); + assertEquals(options, ((CheckBox) gui2.forms.get("checkbox_1")).getOptions()); + } +} diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/InputTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/InputTest.java index b6f1e3e783..d15fab4a57 100644 --- a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/InputTest.java +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/InputTest.java @@ -20,16 +20,19 @@ package org.apache.zeppelin.display; import java.util.HashMap; import java.util.Map; +import org.apache.zeppelin.display.ui.CheckBox; +import org.apache.zeppelin.display.ui.OptionInput.ParamOption; +import org.apache.zeppelin.display.ui.Select; +import org.apache.zeppelin.display.ui.TextBox; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertNull; -import org.apache.zeppelin.display.Input.ParamOption; - public class InputTest { @Before @@ -42,7 +45,7 @@ public class InputTest { @Test public void testFormExtraction() { - // input form + // textbox form String script = "${input_form=}"; Map forms = Input.extractSimpleQueryForm(script); assertEquals(1, forms.size()); @@ -50,50 +53,57 @@ public class InputTest { assertEquals("input_form", form.name); assertNull(form.displayName); assertEquals("", form.defaultValue); - assertNull(form.options); + assertTrue(form instanceof TextBox); - // input form with display name & default value + // textbox form with display name & default value script = "${input_form(Input Form)=xxx}"; forms = Input.extractSimpleQueryForm(script); form = forms.get("input_form"); assertEquals("xxx", form.defaultValue); + assertTrue(form instanceof TextBox); // selection form script = "${select_form(Selection Form)=op1,op1|op2(Option 2)|op3}"; form = Input.extractSimpleQueryForm(script).get("select_form"); assertEquals("select_form", form.name); assertEquals("op1", form.defaultValue); + assertTrue(form instanceof Select); assertArrayEquals(new ParamOption[]{new ParamOption("op1", null), - new ParamOption("op2", "Option 2"), new ParamOption("op3", null)}, form.options); + new ParamOption("op2", "Option 2"), new ParamOption("op3", null)}, + ((Select) form).getOptions()); // checkbox form script = "${checkbox:checkbox_form=op1,op1|op2|op3}"; form = Input.extractSimpleQueryForm(script).get("checkbox_form"); assertEquals("checkbox_form", form.name); - assertEquals("checkbox", form.type); + assertTrue(form instanceof CheckBox); + assertArrayEquals(new Object[]{"op1"}, (Object[]) form.defaultValue); assertArrayEquals(new ParamOption[]{new ParamOption("op1", null), - new ParamOption("op2", null), new ParamOption("op3", null)}, form.options); + new ParamOption("op2", null), new ParamOption("op3", null)}, + ((CheckBox) form).getOptions()); // checkbox form with multiple default checks script = "${checkbox:checkbox_form(Checkbox Form)=op1|op3,op1(Option 1)|op2|op3}"; form = Input.extractSimpleQueryForm(script).get("checkbox_form"); assertEquals("checkbox_form", form.name); assertEquals("Checkbox Form", form.displayName); - assertEquals("checkbox", form.type); + assertTrue(form instanceof CheckBox); assertArrayEquals(new Object[]{"op1", "op3"}, (Object[]) form.defaultValue); assertArrayEquals(new ParamOption[]{new ParamOption("op1", "Option 1"), - new ParamOption("op2", null), new ParamOption("op3", null)}, form.options); + new ParamOption("op2", null), new ParamOption("op3", null)}, + ((CheckBox) form).getOptions()); // checkbox form with no default check script = "${checkbox:checkbox_form(Checkbox Form)=,op1(Option 1)|op2(Option 2)|op3(Option 3)}"; form = Input.extractSimpleQueryForm(script).get("checkbox_form"); assertEquals("checkbox_form", form.name); assertEquals("Checkbox Form", form.displayName); - assertEquals("checkbox", form.type); + assertTrue(form instanceof CheckBox); assertArrayEquals(new Object[]{}, (Object[]) form.defaultValue); assertArrayEquals(new ParamOption[]{new ParamOption("op1", "Option 1"), - new ParamOption("op2", "Option 2"), new ParamOption("op3", "Option 3")}, form.options); + new ParamOption("op2", "Option 2"), new ParamOption("op3", "Option 3")}, + ((CheckBox) form).getOptions()); } @@ -125,4 +135,5 @@ public class InputTest { assertEquals("INPUT=some_inputSELECTED=s_op2\nCHECKED=c_op1\n" + "NEW_CHECKED=nc_a and nc_c", replaced); } + } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java index 1aa4f2893c..10ed00e7fc 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java @@ -45,6 +45,7 @@ import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars; import org.apache.zeppelin.display.AngularObject; import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.AngularObjectRegistryListener; +import org.apache.zeppelin.display.Input; import org.apache.zeppelin.helium.ApplicationEventListener; import org.apache.zeppelin.helium.HeliumPackage; import org.apache.zeppelin.interpreter.Interpreter; @@ -134,7 +135,9 @@ public class NotebookServer extends WebSocketServlet } } } - }).setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").create(); + }).setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .registerTypeAdapterFactory(Input.TypeAdapterFactory).create(); + final Map> noteSocketMap = new HashMap<>(); final Queue connectedSockets = new ConcurrentLinkedQueue<>(); final Map> userConnectedSockets = new ConcurrentHashMap<>(); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java index 8e09f00259..add23ac0fb 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/ParagraphActionsIT.java @@ -548,7 +548,7 @@ public class ParagraphActionsIT extends AbstractZeppelinIT { try { createNewNote(); - setTextOfParagraph(1, "%spark println(\"Hello \"+z.input(\"name\", \"world\")) "); + setTextOfParagraph(1, "%spark println(\"Hello \"+z.textbox(\"name\", \"world\")) "); runParagraph(1); waitForParagraph(1, "FINISHED"); diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java index eb8186e756..77e0844677 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinSparkClusterTest.java @@ -490,7 +490,7 @@ public class ZeppelinSparkClusterTest extends AbstractTestRestApi { Map config = p.getConfig(); config.put("enabled", true); p.setConfig(config); - String code = "%spark.spark println(z.input(\"my_input\", \"default_name\"))\n" + + String code = "%spark.spark println(z.textbox(\"my_input\", \"default_name\"))\n" + "println(z.select(\"my_select\", \"1\"," + "Seq((\"1\", \"select_1\"), (\"2\", \"select_2\"))))\n" + "val items=z.checkbox(\"my_checkbox\", Seq(\"2\"), " + diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html index 64da3cfff7..249e7c105b 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html @@ -20,7 +20,7 @@ limitations under the License.
+ ng-if="paragraph.settings.forms[formulaire.name].type == 'Select'" + ng-enter="runParagraphFromButton(getEditorValue())" + ng-model="paragraph.settings.params[formulaire.name]" + ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }" + name="{{formulaire.name}}" + ng-options="option.value as (option.displayName||option.value) for option in paragraph.settings.forms[formulaire.name].options">
+ paragraph.settings.forms[formulaire.name].type == 'CheckBox'">
+ paragraph.settings.forms[formulaire.name].type == 'CheckBox'">