zeppelin-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From m...@apache.org
Subject incubator-zeppelin git commit: Add checkbox as a type of dynamic forms
Date Sun, 28 Feb 2016 15:55:36 GMT
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)
![igojnqh0mz](https://cloud.githubusercontent.com/assets/3282033/13136828/929ec1ec-d5d1-11e5-89d9-98659f444f37.gif)

### 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 <wangzhong.neu@gmail.com>
Author: Zhong Wang <zhongwang@Zhongs-MacBook-Pro.local>

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 <wangzhong.neu@gmail.com>
Authored: Thu Feb 25 21:30:57 2016 -0800
Committer: Lee moon soo <moon@apache.org>
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
 */


Mime
View raw message