Repository: incubator-zeppelin Updated Branches: refs/heads/master b960f09d0 -> 4df083dca
Add checkbox as a type of dynamic forms ### What is this PR for? 1. Add checkbox support in dynamic forms 2. Fix ZEPPELIN-699: Cannot select the first item of dynamic forms when it is just created ### What type of PR is it? Feature & Bug Fix ### Todos * [x] Compatibility test with notes from previous versions * [x] Documentation * [x] Nice CSS layout * [x] Support checkbox creation by string substitution * [x] Support pyspark ### Is there a relevant Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-671 https://issues.apache.org/jira/browse/ZEPPELIN-669 ### How should this be tested? 1. Create a %spark paragraph: `val a = z.checkbox("my_check", Seq(("a", "a"), ("b", "b"), ("c", "c")))` 2. Toggle the checkboxes and see the outcome ### Screenshots (if appropriate)  ### Questions: * Does the licenses files need update? NO * Is there breaking changes for older versions? Sort of. I am not sure whether Input.type (form.type) has already been used in other places * Does this needs documentation? YES Author: Zhong Wang <[email protected]> Author: Zhong Wang <[email protected]> Closes #713 from zhongneu/dynamic-forms-checkbox and squashes the following commits: 0d3e566 [Zhong Wang] change css class name from checkbox-group to checkbox-item 584bab8 [Zhong Wang] some cleanups & fix an issue of obsolete values 790d0f0 [Zhong Wang] add pyspark support 2af3e64 [Zhong Wang] fix docs c0683f1 [Zhong Wang] add documentation for checkbox forms 9336b61 [Zhong Wang] refactoring the form parsing / substitution code to support delimiter for multi-selection forms 8035cb1 [Zhong Wang] fix a display issue in query mode if no display name is specified 2a634be [Zhong Wang] fix an issue with invalid options: related to ZEPPELIN-674 db62ca7 [Zhong Wang] revoke changes for hide/show; improve compatibility of older versions of notebooks b799fb0 [Zhong Wang] add option to configure hidden behavior for checkboxes f10d6e2 [Zhong Wang] better CSS layout & add show/hide option 3273230 [Zhong Wang] fix a bug: the checkbox should show display name e190707 [Zhong Wang] fix several bugs, including ZEPPELIN-669 6969e8c [Zhong Wang] first attempt of adding checkbox to dynamic forms Project: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/commit/4df083dc Tree: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/tree/4df083dc Diff: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/diff/4df083dc Branch: refs/heads/master Commit: 4df083dca9e0965b42d9dce9912e902d9afe3163 Parents: b960f09 Author: Zhong Wang <[email protected]> Authored: Thu Feb 25 21:30:57 2016 -0800 Committer: Lee moon soo <[email protected]> Committed: Sun Feb 28 07:59:05 2016 -0800 ---------------------------------------------------------------------- .../zeppelin/img/screenshots/form_checkbox.png | Bin 0 -> 23030 bytes .../img/screenshots/form_checkbox_delimiter.png | Bin 0 -> 18913 bytes .../img/screenshots/form_checkbox_prog.png | Bin 0 -> 37691 bytes docs/manual/dynamicform.md | 33 +++ .../apache/zeppelin/spark/ZeppelinContext.java | 25 +- .../main/resources/python/zeppelin_pyspark.py | 10 + .../java/org/apache/zeppelin/display/GUI.java | 32 ++- .../java/org/apache/zeppelin/display/Input.java | 259 ++++++++++++------- .../org/apache/zeppelin/display/InputTest.java | 93 ++++++- .../paragraph-parameterizedQueryForm.html | 13 +- .../notebook/paragraph/paragraph.controller.js | 13 +- .../src/app/notebook/paragraph/paragraph.css | 9 + 12 files changed, 377 insertions(+), 110 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/docs/assets/themes/zeppelin/img/screenshots/form_checkbox.png ---------------------------------------------------------------------- diff --git a/docs/assets/themes/zeppelin/img/screenshots/form_checkbox.png b/docs/assets/themes/zeppelin/img/screenshots/form_checkbox.png new file mode 100644 index 0000000..ffc83cc Binary files /dev/null and b/docs/assets/themes/zeppelin/img/screenshots/form_checkbox.png differ http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/docs/assets/themes/zeppelin/img/screenshots/form_checkbox_delimiter.png ---------------------------------------------------------------------- diff --git a/docs/assets/themes/zeppelin/img/screenshots/form_checkbox_delimiter.png b/docs/assets/themes/zeppelin/img/screenshots/form_checkbox_delimiter.png new file mode 100644 index 0000000..ed58f0e Binary files /dev/null and b/docs/assets/themes/zeppelin/img/screenshots/form_checkbox_delimiter.png differ http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/docs/assets/themes/zeppelin/img/screenshots/form_checkbox_prog.png ---------------------------------------------------------------------- diff --git a/docs/assets/themes/zeppelin/img/screenshots/form_checkbox_prog.png b/docs/assets/themes/zeppelin/img/screenshots/form_checkbox_prog.png new file mode 100644 index 0000000..57b52da Binary files /dev/null and b/docs/assets/themes/zeppelin/img/screenshots/form_checkbox_prog.png differ http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/docs/manual/dynamicform.md ---------------------------------------------------------------------- diff --git a/docs/manual/dynamicform.md b/docs/manual/dynamicform.md index 5b754f0..0622287 100644 --- a/docs/manual/dynamicform.md +++ b/docs/manual/dynamicform.md @@ -54,6 +54,16 @@ Also you can separate option's display name and value, using _${formName=default <img src="/assets/themes/zeppelin/img/screenshots/form_select_displayname.png" /> +#### Checkbox form + +For multi-selection, you can create a checkbox form using _${checkbox:formName=defaultValue1|defaultValue2...,option1|option2...}_. The variable will be substituted by a comma-separated string based on the selected items. For example: + +<img src="/assets/themes/zeppelin/img/screenshots/form_checkbox.png"> + +Besides, you can specify the delimiter using _${checkbox(delimiter):formName=...}_: + +<img src="/assets/themes/zeppelin/img/screenshots/form_checkbox_delimiter.png"> + ### Creates Programmatically Some language backend uses programmatic way to create form. For example [ZeppelinContext](../interpreter/spark.html#zeppelincontext) provides form creation API @@ -134,3 +144,26 @@ print("Hello "+z.select("day", [("1","mon"), </div> </div> <img src="/assets/themes/zeppelin/img/screenshots/form_select_prog.png" /> + +#### Checkbox form +<div class="codetabs"> + <div data-lang="scala" markdown="1"> + +{% highlight scala %} +%spark +val options = Seq(("apple","Apple"), ("banana","Banana"), ("orange","Orange")) +println("Hello "+z.checkbox("fruit", options).mkString(" and ")) +{% endhighlight %} + + </div> + <div data-lang="python" markdown="1"> + +{% highlight python %} +%pyspark +options = [("apple","Apple"), ("banana","Banana"), ("orange","Orange")] +print("Hello "+ " and ".join(z.checkbox("fruit", options, ["apple"]))) +{% endhighlight %} + + </div> +</div> +<img src="/assets/themes/zeppelin/img/screenshots/form_checkbox_prog.png" /> http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java ---------------------------------------------------------------------- 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 a25c2c2..88094b5 100644 --- a/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java +++ b/spark/src/main/java/org/apache/zeppelin/spark/ZeppelinContext.java @@ -17,7 +17,9 @@ package org.apache.zeppelin.spark; +import static scala.collection.JavaConversions.asJavaCollection; import static scala.collection.JavaConversions.asJavaIterable; +import static scala.collection.JavaConversions.asScalaIterable; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -84,6 +86,27 @@ public class ZeppelinContext { public Object select(String name, Object defaultValue, scala.collection.Iterable<Tuple2<Object, String>> options) { + return gui.select(name, defaultValue, tuplesToParamOptions(options)); + } + + public scala.collection.Iterable<Object> checkbox(String name, + scala.collection.Iterable<Tuple2<Object, String>> options) { + List<Object> allChecked = new LinkedList<Object>(); + for (Tuple2<Object, String> option : asJavaIterable(options)) { + allChecked.add(option._1()); + } + return checkbox(name, asScalaIterable(allChecked), options); + } + + public scala.collection.Iterable<Object> checkbox(String name, + scala.collection.Iterable<Object> defaultChecked, + scala.collection.Iterable<Tuple2<Object, String>> options) { + return asScalaIterable(gui.checkbox(name, asJavaCollection(defaultChecked), + tuplesToParamOptions(options))); + } + + private ParamOption[] tuplesToParamOptions( + scala.collection.Iterable<Tuple2<Object, String>> options) { int n = options.size(); ParamOption[] paramOptions = new ParamOption[n]; Iterator<Tuple2<Object, String>> it = asJavaIterable(options).iterator(); @@ -94,7 +117,7 @@ public class ZeppelinContext { paramOptions[i++] = new ParamOption(valueAndDisplayValue._1(), valueAndDisplayValue._2()); } - return gui.select(name, defaultValue, paramOptions); + return paramOptions; } public void setGui(GUI o) { http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/spark/src/main/resources/python/zeppelin_pyspark.py ---------------------------------------------------------------------- diff --git a/spark/src/main/resources/python/zeppelin_pyspark.py b/spark/src/main/resources/python/zeppelin_pyspark.py index 7da0f4e..9b94274 100644 --- a/spark/src/main/resources/python/zeppelin_pyspark.py +++ b/spark/src/main/resources/python/zeppelin_pyspark.py @@ -84,6 +84,16 @@ class PyZeppelinContext(dict): iterables = gateway.jvm.scala.collection.JavaConversions.collectionAsScalaIterable(tuples) return self.z.select(name, defaultValue, iterables) + def checkbox(self, name, options, defaultChecked = None): + if defaultChecked is None: + defaultChecked = map(lambda items: items[0], options) + optionTuples = 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) + def __tupleToScalaTuple2(self, tuple): if (len(tuple) == 2): return gateway.jvm.scala.Tuple2(tuple[0], tuple[1]) http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/GUI.java ---------------------------------------------------------------------- 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 3772610..42a5584 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 @@ -18,7 +18,10 @@ package org.apache.zeppelin.display; import java.io.Serializable; + +import java.util.Collection; import java.util.HashMap; +import java.util.LinkedList; import java.util.Map; import java.util.TreeMap; @@ -59,7 +62,7 @@ public class GUI implements Serializable { value = defaultValue; } - forms.put(id, new Input(id, defaultValue)); + forms.put(id, new Input(id, defaultValue, "input")); return value; } @@ -72,10 +75,35 @@ public class GUI implements Serializable { if (value == null) { value = defaultValue; } - forms.put(id, new Input(id, defaultValue, options)); + forms.put(id, new Input(id, defaultValue, "select", options)); return value; } + public Collection<Object> checkbox(String id, Collection<Object> defaultChecked, + ParamOption[] options) { + Collection<Object> checked = (Collection<Object>) params.get(id); + if (checked == null) { + checked = defaultChecked; + } + forms.put(id, new Input(id, defaultChecked, "checkbox", options)); + Collection<Object> filtered = new LinkedList<Object>(); + for (Object o : checked) { + if (isValidOption(o, options)) { + filtered.add(o); + } + } + return filtered; + } + + private boolean isValidOption(Object o, ParamOption[] options) { + for (ParamOption option : options) { + if (o.equals(option.getValue())) { + return true; + } + } + return false; + } + public void clear() { this.forms = new TreeMap<String, Input>(); } http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/zeppelin-interpreter/src/main/java/org/apache/zeppelin/display/Input.java ---------------------------------------------------------------------- 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 3b1b1d2..bb0aa4d 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 @@ -17,8 +17,12 @@ package org.apache.zeppelin.display; +import org.apache.commons.lang.StringUtils; + import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -43,6 +47,25 @@ public class Input implements Serializable { 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; } @@ -64,29 +87,32 @@ public class Input implements Serializable { String name; String displayName; String type; + String argument; Object defaultValue; ParamOption[] options; boolean hidden; - public Input(String name, Object defaultValue) { + 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, ParamOption[] options) { + 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, Object defaultValue, + 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; @@ -142,6 +168,18 @@ public class Input implements Serializable { return hidden; } + // 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 + // ${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 + // expanded to "US or JP" + private static final Pattern VAR_PTN = Pattern.compile("([_])?[$][{]([^=}]*([=][^}]*)?)[}]"); private static String[] getNameAndDisplayName(String str) { Pattern p = Pattern.compile("([^(]*)\\s*[(]([^)]*)[)]"); @@ -156,140 +194,161 @@ public class Input implements Serializable { } private static String[] getType(String str) { - Pattern p = Pattern.compile("([^:]*)\\s*:\\s*(.*)"); + Pattern p = Pattern.compile("([^:()]*)\\s*([(][^()]*[)])?\\s*:(.*)"); Matcher m = p.matcher(str.trim()); if (m == null || m.find() == false) { return null; } - String[] ret = new String[2]; + String[] ret = new String[3]; ret[0] = m.group(1).trim(); - ret[1] = m.group(2).trim(); + if (m.group(2) != null) { + ret[1] = m.group(2).trim().replaceAll("[()]", ""); + } + ret[2] = m.group(3).trim(); return ret; } - public static Map<String, Input> extractSimpleQueryParam(String script) { - Map<String, Input> params = new HashMap<String, Input>(); - if (script == null) { - return params; + private static Input getInputForm(Matcher match) { + String hiddenPart = match.group(1); + boolean hidden = false; + if ("_".equals(hiddenPart)) { + hidden = true; } - String replaced = script; + String m = match.group(2); - Pattern pattern = Pattern.compile("([_])?[$][{]([^=}]*([=][^}]*)?)[}]"); + String namePart; + String valuePart; - Matcher match = pattern.matcher(replaced); - while (match.find()) { - String hiddenPart = match.group(1); - boolean hidden = false; - if ("_".equals(hiddenPart)) { - hidden = true; - } - String m = match.group(2); + int p = m.indexOf('='); + if (p > 0) { + namePart = m.substring(0, p); + valuePart = m.substring(p + 1); + } else { + namePart = m; + valuePart = null; + } - String namePart; - String valuePart; - - int p = m.indexOf('='); - if (p > 0) { - namePart = m.substring(0, p); - valuePart = m.substring(p + 1); - } else { - namePart = m; - valuePart = null; - } + String varName; + String displayName = null; + String type = null; + String arg = null; + Object defaultValue = ""; + ParamOption[] paramOptions = null; + + // get var name type + String varNamePart; + String[] typeArray = getType(namePart); + if (typeArray != null) { + type = typeArray[0]; + arg = typeArray[1]; + varNamePart = typeArray[2]; + } else { + varNamePart = namePart; + } - String varName; - String displayName = null; - String type = null; - String defaultValue = ""; - ParamOption[] paramOptions = null; + // get var name and displayname + String[] varNameArray = getNameAndDisplayName(varNamePart); + if (varNameArray != null) { + varName = varNameArray[0]; + displayName = varNameArray[1]; + } else { + varName = varNamePart.trim(); + } - // get var name type - String varNamePart; - String[] typeArray = getType(namePart); - if (typeArray != null) { - type = typeArray[0]; - varNamePart = typeArray[1]; - } else { - varNamePart = namePart; - } + // get defaultValue + if (valuePart != null) { + // find default value + int optionP = valuePart.indexOf(","); + if (optionP >= 0) { // option available + defaultValue = valuePart.substring(0, optionP); + if (type != null && type.equals("checkbox")) { + // checkbox may contain multiple default checks + defaultValue = Input.splitPipe((String) defaultValue); + } + String optionPart = valuePart.substring(optionP + 1); + String[] options = Input.splitPipe(optionPart); - // get var name and displayname - String[] varNameArray = getNameAndDisplayName(varNamePart); - if (varNameArray != null) { - varName = varNameArray[0]; - displayName = varNameArray[1]; - } else { - varName = varNamePart.trim(); - } + paramOptions = new ParamOption[options.length]; - // get defaultValue - if (valuePart != null) { - // find default value - int optionP = valuePart.indexOf(","); - if (optionP > 0) { // option available - defaultValue = valuePart.substring(0, optionP); - String optionPart = valuePart.substring(optionP + 1); - String[] options = Input.splitPipe(optionPart); + for (int i = 0; i < options.length; i++) { - paramOptions = new ParamOption[options.length]; + String[] optNameArray = getNameAndDisplayName(options[i]); + if (optNameArray != null) { + paramOptions[i] = new ParamOption(optNameArray[0], optNameArray[1]); + } else { + paramOptions[i] = new ParamOption(options[i], null); + } + } - for (int i = 0; i < options.length; i++) { - String[] optNameArray = getNameAndDisplayName(options[i]); - if (optNameArray != null) { - paramOptions[i] = new ParamOption(optNameArray[0], optNameArray[1]); - } else { - paramOptions[i] = new ParamOption(options[i], null); - } - } + } else { // no option + defaultValue = valuePart; + } + } - } else { // no option - defaultValue = valuePart; - } + return new Input(varName, displayName, type, arg, defaultValue, paramOptions, hidden); + } - } + public static Map<String, Input> extractSimpleQueryParam(String script) { + Map<String, Input> params = new HashMap<String, Input>(); + if (script == null) { + return params; + } + String replaced = script; - Input param = new Input(varName, displayName, type, defaultValue, paramOptions, hidden); - params.put(varName, param); + Matcher match = VAR_PTN.matcher(replaced); + while (match.find()) { + Input param = getInputForm(match); + params.put(param.name, param); } params.remove("pql"); return params; } + private static final String DEFAULT_DELIMITER = ","; + public static String getSimpleQuery(Map<String, Object> params, String script) { String replaced = script; - for (String key : params.keySet()) { - Object value = params.get(key); - replaced = - replaced.replaceAll("[_]?[$][{]([^:]*[:])?" + key + "([(][^)]*[)])?(=[^}]*)?[}]", - value.toString()); - } + Matcher match = VAR_PTN.matcher(replaced); + while (match.find()) { + Input input = getInputForm(match); + Object value; + if (params.containsKey(input.name)) { + value = params.get(input.name); + } else { + value = input.defaultValue; + } - Pattern pattern = Pattern.compile("[$][{]([^=}]*[=][^}]*)[}]"); - while (true) { - Matcher match = pattern.matcher(replaced); - if (match != null && match.find()) { - String m = match.group(1); - int p = m.indexOf('='); - String replacement = m.substring(p + 1); - int optionP = replacement.indexOf(","); - if (optionP > 0) { - replacement = replacement.substring(0, optionP); + String expanded; + if (value instanceof Object[] || value instanceof Collection) { // multi-selection + String delimiter = input.argument; + if (delimiter == null) { + delimiter = DEFAULT_DELIMITER; } - replaced = - replaced.replaceFirst("[_]?[$][{]" - + m.replaceAll("[(]", ".").replaceAll("[)]", ".").replaceAll("[|]", ".") + "[}]", - replacement); - } else { - break; + Collection<Object> checked = value instanceof Collection ? (Collection<Object>) value + : Arrays.asList((Object[]) value); + List<Object> validChecked = new LinkedList<Object>(); + for (Object o : checked) { // filter out obsolete checked values + for (ParamOption option : input.getOptions()) { + if (option.getValue().equals(o)) { + validChecked.add(o); + break; + } + } + } + params.put(input.name, validChecked); + expanded = StringUtils.join(validChecked, delimiter); + } else { // single-selection + expanded = value.toString(); } + replaced = match.replaceFirst(expanded); + match = VAR_PTN.matcher(replaced); } - replaced = replaced.replace("[_]?[$][{]([^=}]*)[}]", ""); return replaced; } http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/zeppelin-interpreter/src/test/java/org/apache/zeppelin/display/InputTest.java ---------------------------------------------------------------------- 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 626ae99..aeb0d83 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 @@ -17,12 +17,19 @@ package org.apache.zeppelin.display; -import java.io.IOException; +import java.util.HashMap; +import java.util.Map; 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.assertNull; + +import org.apache.zeppelin.display.Input.ParamOption; + public class InputTest { @Before @@ -34,6 +41,88 @@ public class InputTest { } @Test - public void testDefaultParamReplace() throws IOException{ + public void testFormExtraction() { + // input form + String script = "${input_form=}"; + Map<String, Input> forms = Input.extractSimpleQueryParam(script); + assertEquals(1, forms.size()); + Input form = forms.get("input_form"); + assertEquals("input_form", form.name); + assertNull(form.displayName); + assertEquals("", form.defaultValue); + assertNull(form.options); + + // input form with display name & default value + script = "${input_form(Input Form)=xxx}"; + forms = Input.extractSimpleQueryParam(script); + form = forms.get("input_form"); + assertEquals("xxx", form.defaultValue); + + // selection form + script = "${select_form(Selection Form)=op1,op1|op2(Option 2)|op3}"; + form = Input.extractSimpleQueryParam(script).get("select_form"); + assertEquals("select_form", form.name); + assertEquals("op1", form.defaultValue); + assertArrayEquals(new ParamOption[]{new ParamOption("op1", null), + new ParamOption("op2", "Option 2"), new ParamOption("op3", null)}, form.options); + + // checkbox form + script = "${checkbox:checkbox_form=op1,op1|op2|op3}"; + form = Input.extractSimpleQueryParam(script).get("checkbox_form"); + assertEquals("checkbox_form", form.name); + assertEquals("checkbox", form.type); + assertArrayEquals(new Object[]{"op1"}, (Object[]) form.defaultValue); + assertArrayEquals(new ParamOption[]{new ParamOption("op1", null), + new ParamOption("op2", null), new ParamOption("op3", null)}, form.options); + + // checkbox form with multiple default checks + script = "${checkbox:checkbox_form(Checkbox Form)=op1|op3,op1(Option 1)|op2|op3}"; + form = Input.extractSimpleQueryParam(script).get("checkbox_form"); + assertEquals("checkbox_form", form.name); + assertEquals("Checkbox Form", form.displayName); + assertEquals("checkbox", form.type); + 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); + + // checkbox form with no default check + script = "${checkbox:checkbox_form(Checkbox Form)=,op1(Option 1)|op2(Option 2)|op3(Option 3)}"; + form = Input.extractSimpleQueryParam(script).get("checkbox_form"); + assertEquals("checkbox_form", form.name); + assertEquals("Checkbox Form", form.displayName); + assertEquals("checkbox", form.type); + 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); + } + + + @Test + public void testFormSubstitution() { + // test form substitution without new forms + String script = "INPUT=${input_form=}SELECTED=${select_form(Selection Form)=,s_op1|s_op2|s_op3}\n" + + "CHECKED=${checkbox:checkbox_form=c_op1|c_op2,c_op1|c_op2|c_op3}"; + Map<String, Object> params = new HashMap<String, Object>(); + params.put("input_form", "some_input"); + params.put("select_form", "s_op2"); + params.put("checkbox_form", new String[]{"c_op1", "c_op3"}); + String replaced = Input.getSimpleQuery(params, script); + assertEquals("INPUT=some_inputSELECTED=s_op2\nCHECKED=c_op1,c_op3", replaced); + + // test form substitution with new forms + script = "INPUT=${input_form=}SELECTED=${select_form(Selection Form)=,s_op1|s_op2|s_op3}\n" + + "CHECKED=${checkbox:checkbox_form=c_op1|c_op2,c_op1|c_op2|c_op3}\n" + + "NEW_CHECKED=${checkbox( and ):new_check=nc_a|nc_c,nc_a|nc_b|nc_c}"; + replaced = Input.getSimpleQuery(params, script); + assertEquals("INPUT=some_inputSELECTED=s_op2\nCHECKED=c_op1,c_op3\n" + + "NEW_CHECKED=nc_a and nc_c", replaced); + + // test form substitution with obsoleted values + script = "INPUT=${input_form=}SELECTED=${select_form(Selection Form)=,s_op1|s_op2|s_op3}\n" + + "CHECKED=${checkbox:checkbox_form=c_op1|c_op2,c_op1|c_op2|c_op3_new}\n" + + "NEW_CHECKED=${checkbox( and ):new_check=nc_a|nc_c,nc_a|nc_b|nc_c}"; + replaced = Input.getSimpleQuery(params, script); + assertEquals("INPUT=some_inputSELECTED=s_op2\nCHECKED=c_op1\n" + + "NEW_CHECKED=nc_a and nc_c", replaced); } } http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html b/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html index bbcf764..8ecb3c4 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph-parameterizedQueryForm.html @@ -28,13 +28,24 @@ limitations under the License. name="{{formulaire.name}}" /> <select class="form-control input-sm" - ng-if="paragraph.settings.forms[formulaire.name].options" + ng-if="paragraph.settings.forms[formulaire.name].options && paragraph.settings.forms[formulaire.name].type != 'checkbox'" ng-change="runParagraph(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"> </select> + + <div ng-if="paragraph.settings.forms[formulaire.name].type == 'checkbox'"> + <label ng-repeat="option in paragraph.settings.forms[formulaire.name].options" + class="checkbox-item input-sm"> + <input type="checkbox" + ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }" + ng-checked="paragraph.settings.params[formulaire.name].indexOf(option.value) > -1" + ng-click="toggleCheckbox(formulaire, option, false)"/>{{option.displayName||option.value}} + </label> + </div> + </div> </div> </form> http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index 3b22b5b..3935cfc 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -629,13 +629,18 @@ angular.module('zeppelinWebApp') value = params[formulaire.name]; } - if (value === '') { - value = formulaire.options[0].value; - } - $scope.paragraph.settings.params[formulaire.name] = value; }; + $scope.toggleCheckbox = function(formulaire, option) { + var idx = $scope.paragraph.settings.params[formulaire.name].indexOf(option.value); + if (idx > -1) { + $scope.paragraph.settings.params[formulaire.name].splice(idx, 1); + } else { + $scope.paragraph.settings.params[formulaire.name].push(option.value); + } + }; + $scope.aceChanged = function() { $scope.dirtyText = $scope.editor.getSession().getValue(); $scope.startSaveTimer(); http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/4df083dc/zeppelin-web/src/app/notebook/paragraph/paragraph.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.css b/zeppelin-web/src/app/notebook/paragraph/paragraph.css index 3b56c2a..f170ca0 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.css +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.css @@ -234,6 +234,15 @@ padding-left: 0; } +.paragraphForm.form-horizontal .form-group .checkbox-item { + padding-left: 0; + padding-right: 10px; +} + +.paragraphForm.form-horizontal .form-group .checkbox-item input { + margin-right: 2px; +} + /* Ace Text Editor CSS */
