ZEPPELIN-2395. Refactor Input.java to make dynamic forms extensible

This commit is contained in:
Jeff Zhang 2017-04-12 10:50:16 +08:00
parent 4b1b521fc3
commit 16d42a848e
29 changed files with 791 additions and 156 deletions

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -93,6 +93,7 @@
<log4j.version>1.2.17</log4j.version>
<libthrift.version>0.9.2</libthrift.version>
<gson.version>2.2</gson.version>
<gson-extras.version>0.2.1</gson-extras.version>
<guava.version>15.0</guava.version>
<jetty.version>9.2.15.v20160210</jetty.version>
<httpcomponents.core.version>4.3.3</httpcomponents.core.version>
@ -192,6 +193,12 @@
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>org.danilopianini</groupId>
<artifactId>gson-extras</artifactId>
<version>${gson-extras.version}</version>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>

View file

@ -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

View file

@ -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

View file

@ -61,6 +61,11 @@
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.danilopianini</groupId>
<artifactId>gson-extras</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>

View file

@ -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<String, Object> params = new HashMap<>(); // form parameters from client
LinkedHashMap<String, Input> 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<Object> 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<String, Input> 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;
}
}

View file

@ -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 <T>
*/
public class Input implements Serializable {
/**
* Parameters option.
*/
public static class ParamOption {
Object value;
String displayName;
public class Input<T> 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<String, Input> 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<Object> 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;

View file

@ -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<Object> {
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);
}
}
}

View file

@ -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 <T>
*/
public class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, 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 <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
return new RuntimeTypeAdapterFactory<T>(baseType, typeFieldName);
}
/**
* Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
* the type field name.
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeTypeAdapterFactory<T>(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<T> registerSubtype(Class<? extends T> 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<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
if (type.getRawType() != baseType) {
return null;
}
final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<String, TypeAdapter<?>>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate =
new LinkedHashMap<Class<?>, TypeAdapter<?>>();
for (Map.Entry<String, Class<?>> 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<R>() {
@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<R> delegate = (TypeAdapter<R>) 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<R> delegate = (TypeAdapter<R>) 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<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
Streams.write(clone, out);
}
}.nullSafe();
}
}

View file

@ -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<Object[]> {
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<Object> defaultValue, ParamOption[] options) {
this(name, defaultValue.toArray(), options);
}
}

View file

@ -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 <T>
*/
public abstract class OptionInput<T> extends Input<T> {
/**
* 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;
}
}

View file

@ -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<Object> {
public Select() {
}
public Select(String name, Object defaultValue, ParamOption[] options) {
this.name = name;
this.displayName = name;
this.defaultValue = defaultValue;
this.options = options;
}
}

View file

@ -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<String> {
public TextBox() {
}
public TextBox(String name, String defaultValue) {
this.name = name;
this.displayName = name;
this.defaultValue = defaultValue;
}
}

View file

@ -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<String, Input> currentForms = currentGUI.getForms();
final Map<String, Object> currentParams = currentGUI.getParams();
final GUI remoteGUI = gson.fromJson(remoteResult.getGui(), GUI.class);
final GUI remoteGUI = GUI.fromJson(remoteResult.getGui());
final Map<String, Input> remoteForms = remoteGUI.getForms();
final Map<String, Object> remoteParams = remoteGUI.getParams();
currentForms.putAll(remoteForms);

View file

@ -592,7 +592,7 @@ public class RemoteInterpreterServer
gson.fromJson(ric.getAuthenticationInfo(), AuthenticationInfo.class),
(Map<String, Object>) gson.fromJson(ric.getConfig(),
new TypeToken<Map<String, Object>>() {}.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

View file

@ -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<Object> 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<Object> 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());
}
}

View file

@ -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<String, Input> 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);
}
}

View file

@ -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<String, List<NotebookSocket>> noteSocketMap = new HashMap<>();
final Queue<NotebookSocket> connectedSockets = new ConcurrentLinkedQueue<>();
final Map<String, Queue<NotebookSocket>> userConnectedSockets = new ConcurrentHashMap<>();

View file

@ -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");

View file

@ -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\"), " +

View file

@ -20,7 +20,7 @@ limitations under the License.
<label class="control-label input-sm" ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }">{{formulaire.name}}</label>
<div>
<input class="form-control input-sm"
ng-if="!paragraph.settings.forms[formulaire.name].options"
ng-if="paragraph.settings.forms[formulaire.name].type == 'TextBox'"
ng-enter="runParagraphFromButton(getEditorValue())"
ng-model="paragraph.settings.params[formulaire.name]"
ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }"
@ -28,7 +28,7 @@ limitations under the License.
</div>
<div ng-if="paragraph.config.runOnSelectionChange == true">
<select class="form-control input-sm"
ng-if="paragraph.settings.forms[formulaire.name].options && paragraph.settings.forms[formulaire.name].type != 'checkbox'"
ng-if="paragraph.settings.forms[formulaire.name].type == 'Select'"
ng-change="runParagraphFromButton(getEditorValue())"
ng-model="paragraph.settings.params[formulaire.name]"
ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }"
@ -38,16 +38,16 @@ limitations under the License.
</div>
<div ng-if="paragraph.config.runOnSelectionChange == false">
<select class="form-control input-sm"
ng-if="paragraph.settings.forms[formulaire.name].options && paragraph.settings.forms[formulaire.name].type != 'checkbox'"
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">
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">
</select>
</div>
<div ng-if="paragraph.config.runOnSelectionChange == true &&
paragraph.settings.forms[formulaire.name].type == 'checkbox'">
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"
@ -57,7 +57,7 @@ limitations under the License.
</label>
</div>
<div ng-if="paragraph.config.runOnSelectionChange == false &&
paragraph.settings.forms[formulaire.name].type == 'checkbox'">
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"

View file

@ -54,6 +54,7 @@ function websocketEvents($rootScope, $websocket, $location, baseUrlSrv) {
if (event.data) {
payload = angular.fromJson(event.data);
}
console.log('Receive Json << %o', event.data)
console.log('Receive << %o, %o', payload.op, payload);
var op = payload.op;
var data = payload.data;

View file

@ -26,6 +26,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import com.google.gson.GsonBuilder;
import org.apache.commons.lang.StringUtils;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.display.AngularObject;
@ -54,6 +55,9 @@ import com.google.gson.Gson;
public class Note implements Serializable, ParagraphJobListener {
private static final Logger logger = LoggerFactory.getLogger(Note.class);
private static final long serialVersionUID = 7920699076577612429L;
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(Input.TypeAdapterFactory)
.create();
// threadpool for delayed persist of note
private static final ScheduledThreadPoolExecutor delayedPersistThreadPool =
@ -882,4 +886,19 @@ public class Note implements Serializable, ParagraphJobListener {
this.noteEventListener = noteEventListener;
}
public String toJson() {
return gson.toJson(this);
}
public static Note fromJson(String json) {
Note note = gson.fromJson(json, Note.class);
convertOldInput(note);
return note;
}
private static void convertOldInput(Note note) {
for (Paragraph p : note.paragraphs) {
p.settings.convertOldInput();
}
}
}

View file

@ -138,7 +138,7 @@ public class AzureNotebookRepo implements NotebookRepo {
Gson gson = gsonBuilder.registerTypeAdapter(Date.class, new NotebookImportDeserializer())
.create();
Note note = gson.fromJson(json, Note.class);
Note note = Note.fromJson(json);
for (Paragraph p : note.getParagraphs()) {
if (p.getStatus() == Job.Status.PENDING || p.getStatus() == Job.Status.RUNNING) {

View file

@ -202,7 +202,7 @@ public class S3NotebookRepo implements NotebookRepo {
Note note;
try (InputStream ins = s3object.getObjectContent()) {
String json = IOUtils.toString(ins, conf.getString(ConfVars.ZEPPELIN_ENCODING));
note = gson.fromJson(json, Note.class);
note = Note.fromJson(json);
}
for (Paragraph p : note.getParagraphs()) {

View file

@ -175,7 +175,7 @@ public class VFSNotebookRepo implements NotebookRepo {
String json = IOUtils.toString(ins, conf.getString(ConfVars.ZEPPELIN_ENCODING));
ins.close();
Note note = gson.fromJson(json, Note.class);
Note note = Note.fromJson(json);
// note.setReplLoader(replLoader);
// note.jobListenerFactory = jobListenerFactory;