Merge branch 'master' into ZEPPELIN-732-up

This commit is contained in:
Lee moon soo 2016-05-19 11:41:55 -07:00
commit e89177ec8f
65 changed files with 1150 additions and 299 deletions

View file

@ -81,11 +81,17 @@ before_script:
script:
- mvn $TEST_FLAG $PROFILE -B $TEST_PROJECTS
after_success:
- echo "Travis exited with ${TRAVIS_TEST_RESULT}"
after_failure:
- echo "Travis exited with ${TRAVIS_TEST_RESULT}"
- cat target/rat.txt
- cat zeppelin-server/target/rat.txt
- cat zeppelin-distribution/target/zeppelin-*-SNAPSHOT/zeppelin-*-SNAPSHOT/logs/zeppelin*.log
- cat zeppelin-distribution/target/zeppelin-*-SNAPSHOT/zeppelin-*-SNAPSHOT/logs/zeppelin*.out
- cat zeppelin-web/npm-debug.log
after_script:
- ./testing/stopSparkCluster.sh $SPARK_VER $HADOOP_VER

144
README.md
View file

@ -1,4 +1,4 @@
#Zeppelin
#Zeppelin
**Documentation:** [User Guide](http://zeppelin.incubator.apache.org/docs/latest/index.html)<br/>
**Mailing Lists:** [User and Dev mailing list](http://zeppelin.incubator.apache.org/community.html)<br/>
@ -18,15 +18,16 @@ Core feature:
To know more about Zeppelin, visit our web site [http://zeppelin.incubator.apache.org](http://zeppelin.incubator.apache.org)
## Requirements
* Git
* Java 1.7
* Tested on Mac OSX, Ubuntu 14.X, CentOS 6.X
* Tested on Mac OSX, Ubuntu 14.X, CentOS 6.X, Windows 7 Pro SP1
* Maven (if you want to build from the source code)
* Node.js Package Manager
* Node.js Package Manager (npm, downloaded by Maven during build phase)
## Getting Started
### Before Build
If you don't have requirements prepared, install it.
If you don't have requirements prepared, install it.
(The installation method may vary according to your environment, example is for Ubuntu.)
```
@ -35,17 +36,52 @@ sudo apt-get install git
sudo apt-get install openjdk-7-jdk
sudo apt-get install npm
sudo apt-get install libfontconfig
```
# install maven
#### Proxy settings (optional)
If you are behind a corporate Proxy with NTLM authentication you can use [Cntlm Authentication Proxy](http://cntlm.sourceforge.net/) .
Before build start, run these commands from shell.
```
export http_proxy=http://localhost:3128
export https_proxy=http://localhost:3128
export HTTP_PROXY=http://localhost:3128
export HTTPS_PROXY=http://localhost:3128
npm config set proxy http://localhost:3128
npm config set https-proxy http://localhost:3128
npm config set registry "http://registry.npmjs.org/"
npm config set strict-ssl false
npm cache clean
git config --global http.proxy http://localhost:3128
git config --global https.proxy http://localhost:3128
git config --global url."http://".insteadOf git://
```
After build is complete, run these commands to cleanup.
```
npm config rm proxy
npm config rm https-proxy
git config --global --unset http.proxy
git config --global --unset https.proxy
git config --global --unset url."http://".insteadOf
```
_Notes:_
- If you are on Windows replace `export` with `set` to set env variables
- Replace `localhost:3128` with standard pattern `http://user:pwd@host:port`
- Git configuration is needed because Bower use it for fetching from GitHub
#### Install maven
```
wget http://www.eu.apache.org/dist/maven/maven-3/3.3.3/binaries/apache-maven-3.3.3-bin.tar.gz
sudo tar -zxf apache-maven-3.3.3-bin.tar.gz -C /usr/local/
sudo ln -s /usr/local/apache-maven-3.3.3/bin/mvn /usr/local/bin/mvn
```
_Notes:_
_Notes:_
- Ensure node is installed by running `node --version`
- Ensure maven is running version 3.1.x or higher with `mvn -version`
- Configure maven to use more memory than usual by ```export MAVEN_OPTS="-Xmx2g -XX:MaxPermSize=1024m"```
- Configure maven to use more memory than usual by `export MAVEN_OPTS="-Xmx2g -XX:MaxPermSize=1024m"`
### Build
If you want to build Zeppelin from the source, please first clone this repository, then:
@ -61,7 +97,7 @@ Each Interpreter requires different Options.
To build with a specific Spark version, Hadoop version or specific features, define one or more of the following profiles and options:
##### -Pspark-[version]
##### `-Pspark-[version]`
Set spark major version
@ -84,7 +120,7 @@ Available profiles are
minor version can be adjusted by `-Dspark.version=x.x.x`
##### -Phadoop-[version]
##### `-Phadoop-[version]`
set hadoop major version
@ -101,25 +137,32 @@ Available profiles are
minor version can be adjusted by `-Dhadoop.version=x.x.x`
##### -Pyarn (optional)
##### `-Pyarn` (optional)
enable YARN support for local mode
> YARN for local mode is not supported for Spark v1.5.0 or higher. Set SPARK_HOME instead.
> YARN for local mode is not supported for Spark v1.5.0 or higher. Set `SPARK_HOME` instead.
##### -Ppyspark (optional)
##### `-Ppyspark` (optional)
enable PySpark support for local mode
enable [PySpark](http://spark.apache.org/docs/latest/api/python/) support for local mode.
##### `-Pr` (optional)
##### -Pvendor-repo (optional)
enable [R](https://www.r-project.org/) support with [SparkR](https://spark.apache.org/docs/latest/sparkr.html) integration.
##### `-Psparkr` (optional)
another [R](https://www.r-project.org/) support with [SparkR](https://spark.apache.org/docs/latest/sparkr.html) integration as well as local mode support.
##### `-Pvendor-repo` (optional)
enable 3rd party vendor repository (cloudera)
##### -Pmapr[version] (optional)
##### `-Pmapr[version]` (optional)
For the MapR Hadoop Distribution, these profiles will handle the Hadoop version. As MapR allows different versions
of Spark to be installed, you should specify which version of Spark is installed on the cluster by adding a Spark profile (-Pspark-1.2, -Pspark-1.3, etc.) as needed. For Hive, check the hive/pom.xml and adjust the version installed as well. The correct Maven
For the MapR Hadoop Distribution, these profiles will handle the Hadoop version. As MapR allows different versions of Spark to be installed, you should specify which version of Spark is installed on the cluster by adding a Spark profile (`-Pspark-1.2`, `-Pspark-1.3`, etc.) as needed.
For Hive, check the hive/pom.xml and adjust the version installed as well. The correct Maven
artifacts can be found for every version of MapR at http://doc.mapr.com
Available profiles are
@ -142,7 +185,7 @@ Bulid examples under zeppelin-examples directory
Here're some examples:
```
```sh
# basic build
mvn clean package -Pspark-1.6 -Phadoop-2.4 -Pyarn -Ppyspark
@ -159,13 +202,13 @@ mvn clean package -Pspark-1.5 -Pmapr50 -DskipTests
#### Ignite Interpreter
```
```sh
mvn clean package -Dignite.version=1.1.0-incubating -DskipTests
```
#### Scalding Interpreter
```
```sh
mvn clean package -Pscalding -DskipTests
```
@ -177,67 +220,80 @@ If you wish to configure Zeppelin option (like port number), configure the follo
./conf/zeppelin-env.sh
./conf/zeppelin-site.xml
```
(You can copy ```./conf/zeppelin-env.sh.template``` into ```./conf/zeppelin-env.sh```.
Same for ```zeppelin-site.xml```.)
(You can copy `./conf/zeppelin-env.sh.template` into `./conf/zeppelin-env.sh`.
Same for `zeppelin-site.xml`.)
#### Setting SPARK_HOME and HADOOP_HOME
Without SPARK_HOME and HADOOP_HOME, Zeppelin uses embedded Spark and Hadoop binaries that you have specified with mvn build option.
If you want to use system provided Spark and Hadoop, export SPARK_HOME and HADOOP_HOME in zeppelin-env.sh
Without `SPARK_HOME` and `HADOOP_HOME`, Zeppelin uses embedded Spark and Hadoop binaries that you have specified with mvn build option.
If you want to use system provided Spark and Hadoop, export `SPARK_HOME` and `HADOOP_HOME` in `zeppelin-env.sh`.
You can use any supported version of spark without rebuilding Zeppelin.
```
```sh
# ./conf/zeppelin-env.sh
export SPARK_HOME=...
export HADOOP_HOME=...
```
#### External cluster configuration
Mesos
# ./conf/zeppelin-env.sh
export MASTER=mesos://...
export ZEPPELIN_JAVA_OPTS="-Dspark.executor.uri=/path/to/spark-*.tgz" or SPARK_HOME="/path/to/spark_home"
export MESOS_NATIVE_LIBRARY=/path/to/libmesos.so
```sh
# ./conf/zeppelin-env.sh
export MASTER=mesos://...
export ZEPPELIN_JAVA_OPTS="-Dspark.executor.uri=/path/to/spark-*.tgz" or SPARK_HOME="/path/to/spark_home"
export MESOS_NATIVE_LIBRARY=/path/to/libmesos.so
```
If you set `SPARK_HOME`, you should deploy spark binary on the same location to all worker nodes. And if you set `spark.executor.uri`, every worker can read that file on its node.
Yarn
# ./conf/zeppelin-env.sh
export SPARK_HOME=/path/to/spark_dir
```sh
# ./conf/zeppelin-env.sh
export SPARK_HOME=/path/to/spark_dir
```
### Run
./bin/zeppelin-daemon.sh start
browse localhost:8080 in your browser.
```sh
./bin/zeppelin-daemon.sh start
```
And browse [localhost:8080](localhost:8080) in your browser.
For configuration details check __./conf__ subdirectory.
For configuration details check __`./conf`__ subdirectory.
### Package
To package the final distribution including the compressed archive, run:
mvn clean package -Pbuild-distr
```sh
mvn clean package -Pbuild-distr
```
To build a distribution with specific profiles, run:
mvn clean package -Pbuild-distr -Pspark-1.5 -Phadoop-2.4 -Pyarn -Ppyspark
```sh
mvn clean package -Pbuild-distr -Pspark-1.5 -Phadoop-2.4 -Pyarn -Ppyspark
```
The profiles `-Pspark-1.5 -Phadoop-2.4 -Pyarn -Ppyspark` can be adjusted if you wish to build to a specific spark versions, or omit support such as `yarn`.
The archive is generated under _zeppelin-distribution/target_ directory
The archive is generated under _`zeppelin-distribution/target`_ directory
###Run end-to-end tests
Zeppelin comes with a set of end-to-end acceptance tests driving headless selenium browser
#assumes zeppelin-server running on localhost:8080 (use -Durl=.. to override)
mvn verify
#or take care of starting\stoping zeppelin-server from packaged _zeppelin-distribuion/target_
mvn verify -P using-packaged-distr
```sh
# assumes zeppelin-server running on localhost:8080 (use -Durl=.. to override)
mvn verify
# or take care of starting/stoping zeppelin-server from packaged zeppelin-distribuion/target
mvn verify -P using-packaged-distr
```
[![Analytics](https://ga-beacon.appspot.com/UA-45176241-4/apache/incubator-zeppelin/README.md?pixel)](https://github.com/igrigorik/ga-beacon)

View file

@ -86,9 +86,6 @@ if [[ "${INTERPRETER_ID}" == "spark" ]]; then
SPARK_APP_JAR="$(ls ${ZEPPELIN_HOME}/interpreter/spark/zeppelin-spark*.jar)"
# This will evantually passes SPARK_APP_JAR to classpath of SparkIMain
ZEPPELIN_CLASSPATH+=${SPARK_APP_JAR}
# Need to add the R Interpreter
RZEPPELINPATH="$(ls ${ZEPPELIN_HOME}/interpreter/spark/zeppelin-zr*.jar)"
ZEPPELIN_CLASSPATH+=":${RZEPPELINPATH}"
pattern="$SPARK_HOME/python/lib/py4j-*-src.zip"
py4j=($pattern)
@ -133,8 +130,6 @@ if [[ "${INTERPRETER_ID}" == "spark" ]]; then
ZEPPELIN_CLASSPATH+=":${HADOOP_CONF_DIR}"
fi
RZEPPELINPATH="$(ls ${ZEPPELIN_HOME}/interpreter/spark/zeppelin-zr*.jar)"
ZEPPELIN_CLASSPATH+=":${RZEPPELINPATH}"
export SPARK_CLASSPATH+=":${ZEPPELIN_CLASSPATH}"
fi
elif [[ "${INTERPRETER_ID}" == "hbase" ]]; then

View file

@ -29,6 +29,10 @@ user3 = password4, role2
#ldapRealm.userDnTemplate = cn={0},cn=engg,ou=testdomain,dc=testdomain,dc=com
#ldapRealm.contextFactory.url = ldap://ldaphost:389
#ldapRealm.contextFactory.authenticationMechanism = SIMPLE
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
securityManager.sessionManager = $sessionManager
# 86,400,000 milliseconds = 24 hour
securityManager.sessionManager.globalSessionTimeout = 86400000
shiro.loginUrl = /api/login
[urls]

View file

@ -48,7 +48,7 @@ JIRA_USERNAME = os.environ.get("JIRA_USERNAME", "moon")
# ASF JIRA password
JIRA_PASSWORD = os.environ.get("JIRA_PASSWORD", "00000")
GITHUB_BASE = "https://github.com/apache/incubator-zeppelin/pulls"
GITHUB_BASE = "https://github.com/apache/incubator-zeppelin/pull"
GITHUB_API_BASE = "https://api.github.com/repos/apache/incubator-zeppelin"
JIRA_BASE = "https://issues.apache.org/jira/browse"
JIRA_API_BASE = "https://issues.apache.org/jira"

View file

@ -31,6 +31,8 @@
<!-- li><span><b>Tutorial</b><span></li -->
<li><a href="{{BASE_PATH}}/tutorial/tutorial.html">Tutorial</a></li>
<li role="separator" class="divider"></li>
<li><a href="{{BASE_PATH}}/ui_layout/zeppelin_layout.html">UI Layout</a></li>
<li role="separator" class="divider"></li>
<!-- li><span><b>Guide</b><span></li -->
<li><a href="{{BASE_PATH}}/manual/dynamicform.html">Dynamic Form</a></li>
<li><a href="{{BASE_PATH}}/manual/publish.html">Publish your Paragraph</a></li>
@ -54,7 +56,7 @@
<li><a href="{{BASE_PATH}}/interpreter/lens.html">Lens</a></li>
<li><a href="{{BASE_PATH}}/interpreter/markdown.html">Markdown</a></li>
<li><a href="{{BASE_PATH}}/interpreter/postgresql.html">Postgresql, hawq</a></li>
<li><a href="{{BASE_PATH}}/interpreter/R.html">R</a></li>
<li><a href="{{BASE_PATH}}/interpreter/r.html">R</a></li>
<li><a href="{{BASE_PATH}}/interpreter/scalding.html">Scalding</a></li>
<li><a href="{{BASE_PATH}}/pleasecontribute.html">Shell</a></li>
<li><a href="{{BASE_PATH}}/interpreter/spark.html">Spark</a></li>
@ -70,7 +72,8 @@
<li><a href="{{BASE_PATH}}/displaysystem/display.html">Text</a></li>
<li><a href="{{BASE_PATH}}/displaysystem/display.html#html">Html</a></li>
<li><a href="{{BASE_PATH}}/displaysystem/table.html">Table</a></li>
<li><a href="{{BASE_PATH}}/displaysystem/angular.html">Angular</a></li>
<li><a href="{{BASE_PATH}}/displaysystem/back-end-angular.html">Angular (backend API)</a></li>
<li><a href="{{BASE_PATH}}/displaysystem/front-end-angular.html">Angular (frontend API)</a></li>
</ul>
</li>
<li>
@ -106,4 +109,4 @@
</ul>
</nav><!--/.navbar-collapse -->
</div>
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

View file

@ -1,7 +1,7 @@
---
layout: page
title: "Angular Display System"
description: ""
title: "Angular (backend API)"
description: "Angular (backend API)"
group: display
---
<!--
@ -20,7 +20,7 @@ limitations under the License.
{% include JB/setup %}
## Angular Display System in Zeppelin
## Back-end Angular API in Zeppelin
Angular display system treats output as a view template for [AngularJS](https://angularjs.org/).
It compiles templates and displays them inside of Zeppelin.

View file

@ -0,0 +1,159 @@
---
layout: page
title: "Angular (frontend API)"
description: "Angular (frontend API)"
group: display
---
<!--
Licensed 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.
-->
{% include JB/setup %}
## Front-end Angular API in Zeppelin
In addition to the back-end API to handle Angular objects binding, Zeppelin also exposes a simple AngularJS **`z`** object on the front-end side to expose the same capabilities.
This **`z`** object is accessible in the Angular isolated scope for each paragraph.
<br />
### Bind / Unbind Variables
Through the **`z`**, you can bind / unbind variables to **AngularJS view**
Bind a value to an angular object and a **mandatory** target paragraph:
```html
%angular
<form class="form-inline">
<div class="form-group">
<label for="superheroId">Super Hero: </label>
<input type="text" class="form-control" id="superheroId" placeholder="Superhero name ..." ng-model="superhero"></input>
</div>
<button type="submit" class="btn btn-primary" ng-click="z.angularBind('superhero',superhero,'20160222-232336_1472609686')"> Bind</button>
</form>
```
<img src="../assets/themes/zeppelin/img/screenshots/z_angularBind.gif" />
<hr/>
Unbind/remove a value from angular object and a **mandatory** target paragraph:
```html
%angular
<form class="form-inline">
<button type="submit" class="btn btn-primary" ng-click="z.angularUnbind('superhero','20160222-232336_1472609686')"> UnBind</button>
</form>
```
<img src="../assets/themes/zeppelin/img/screenshots/z_angularUnbind.gif" />
The signature for the **`z.angularBind() / z.angularUnbind()`** functions are:
```javascript
z.angularBind(angularObjectName, angularObjectValue, paragraphId);
z.angularUnbind(angularObjectName, angularObjectValue, paragraphId);
```
All the parameters are mandatory.
<br />
### Run Paragraph
You can also trigger paragraph execution by calling **`z.runParagraph()`** function passing the appropriate paragraphId:
```html
%angular
<form class="form-inline">
<div class="form-group">
<label for="paragraphId">Paragraph Id: </label>
<input type="text" class="form-control" id="paragraphId" placeholder="Paragraph Id ..." ng-model="paragraph"></input>
</div>
<button type="submit" class="btn btn-primary" ng-click="z.runParagraph(paragraph)"> Run Paragraph</button>
</form>
```
<img src="../assets/themes/zeppelin/img/screenshots/z_runParagraph.gif" />
<br />
### Overriding dynamic form with Angular Object
The front-end Angular Interaction API has been designed to offer richer form capabilities and variable binding. With the existing **Dynamic Form** system you can already create input text, select and checkbox forms but the choice is rather limited and the look & feel cannot be changed.
The idea is to create a custom form using plain HTML/AngularJS code and bind actions on this form to push/remove Angular variables to targeted paragraphs using this new API.
Consequently if you use the **Dynamic Form** syntax in a paragraph and there is a bound Angular object having the same name as the _${formName}_, the Angular object will have higher priority and the **Dynamic Form** will not be displayed. Example:
<img src="../assets/themes/zeppelin/img/screenshots/z_angularJs_overriding_dynamic_form.gif" />
<br />
### Feature matrix comparison
How does the front-end AngularJS API compares to the back-end API ? Below is a comparison matrix for both APIs:
<table>
<thead>
<tr>
<th>Actions</th>
<th>Front-end API</th>
<th>Back-end API</th>
</tr>
</thead>
<tr>
<td>Initiate binding</td>
<td>z.angularbind(var, initialValue, paragraphId)</td>
<td>z.angularBind(var, initialValue)</td>
</tr>
<tr>
<td>Update value</td>
<td>same to ordinary angularjs scope variable, or z.angularbind(var, newValue, paragraphId)</td>
<td>z.angularBind(var, newValue)</td>
</tr>
<tr>
<td>Watching value</td>
<td>same to ordinary angularjs scope variable</td>
<td>z.angularWatch(var, (oldVal, newVal) => ...)</td>
</tr>
<tr>
<td>Destroy binding</td>
<td>z.angularUnbind(var, paragraphId)</td>
<td>z.angularUnbind(var)</td>
</tr>
<tr>
<td>Executing Paragraph</td>
<td>z.runParagraph(paragraphId)</td>
<td>z.run(paragraphId)</td>
</tr>
<tbody>
<tbody>
</table>
Both APIs are pretty similar, except for value watching where it is done naturally by AngularJS internals on the front-end and by user custom watcher functions in the back-end.
There is also a slight difference in term of scope. Front-end API limits the Angular object binding to a paragraph scope whereas back-end API allows you to bind an Angular object at the global or note scope. This restriction has been designed purposely to avoid Angular object leaks and scope pollution.

View file

@ -33,7 +33,7 @@ At the "Interpreters" menu, you have to create a new Flink interpreter and provi
</tr>
</table>
For more information about Flink configuration, you can find it [here](https://ci.apache.org/projects/flink/flink-docs-release-0.10/setup/config.html).
For more information about Flink configuration, you can find it [here](https://ci.apache.org/projects/flink/flink-docs-release-1.0/setup/config.html).
## How to test it's working
In example, by using the [Zeppelin notebook](https://www.zeppelinhub.com/viewer/notebooks/aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL05GTGFicy96ZXBwZWxpbi1ub3RlYm9va3MvbWFzdGVyL25vdGVib29rcy8yQVFFREs1UEMvbm90ZS5qc29u) is from Till Rohrmann's presentation [Interactive data analysis with Apache Flink](http://www.slideshare.net/tillrohrmann/data-analysis-49806564) for Apache Flink Meetup.

View file

@ -20,17 +20,22 @@ Spark Interpreter group, which consisted of 4 interpreters.
<tr>
<td>%spark</td>
<td>SparkInterpreter</td>
<td>Creates SparkContext and provides scala environment</td>
<td>Creates a SparkContext and provides a scala environment</td>
</tr>
<tr>
<td>%pyspark</td>
<td>PySparkInterpreter</td>
<td>Provides python environment</td>
<td>Provides a python environment</td>
</tr>
<tr>
<td>%r</td>
<td>SparkRInterpreter</td>
<td>Provides an R environment with SparkR support</td>
</tr>
<tr>
<td>%sql</td>
<td>SparkSQLInterpreter</td>
<td>Provides SQL environment</td>
<td>Provides a SQL environment</td>
</tr>
<tr>
<td>%dep</td>
@ -40,8 +45,8 @@ Spark Interpreter group, which consisted of 4 interpreters.
</table>
## Configuration
Zeppelin provides the below properties for Spark interpreter.
You can also set other Spark properties which are not listed in the table. If so, please refer to [Spark Available Properties](http://spark.apache.org/docs/latest/configuration.html#available-properties).
The Spark interpreter can be configured with properties provided by Zeppelin.
You can also set other Spark properties which are not listed in the table. For a list of additional properties, refer to [Spark Available Properties](http://spark.apache.org/docs/latest/configuration.html#available-properties).
<table class="table-configuration">
<tr>
<th>Property</th>

View file

@ -34,12 +34,12 @@ To create text input form, use _${formName}_ templates.
for example
<img src="/assets/themes/zeppelin/img/screenshots/form_input.png" />
<img src="../assets/themes/zeppelin/img/screenshots/form_input.png" />
Also you can provide default value, using _${formName=defaultValue}_.
<img src="/assets/themes/zeppelin/img/screenshots/form_input_default.png" />
<img src="../assets/themes/zeppelin/img/screenshots/form_input_default.png" />
#### Select form
@ -48,21 +48,21 @@ To create select form, use _${formName=defaultValue,option1|option2...}_
for example
<img src="/assets/themes/zeppelin/img/screenshots/form_select.png" />
<img src="../assets/themes/zeppelin/img/screenshots/form_select.png" />
Also you can separate option's display name and value, using _${formName=defaultValue,option1(DisplayName)|option2(DisplayName)...}_
<img src="/assets/themes/zeppelin/img/screenshots/form_select_displayname.png" />
<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">
<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">
<img src="../assets/themes/zeppelin/img/screenshots/form_checkbox_delimiter.png">
### Creates Programmatically
@ -89,7 +89,7 @@ print("Hello "+z.input("name"))
</div>
</div>
<img src="/assets/themes/zeppelin/img/screenshots/form_input_prog.png" />
<img src="../assets/themes/zeppelin/img/screenshots/form_input_prog.png" />
####Text input form with default value
<div class="codetabs">
@ -110,7 +110,7 @@ print("Hello "+z.input("name", "sun"))
</div>
</div>
<img src="/assets/themes/zeppelin/img/screenshots/form_input_default_prog.png" />
<img src="../assets/themes/zeppelin/img/screenshots/form_input_default_prog.png" />
####Select form
<div class="codetabs">
@ -143,7 +143,7 @@ print("Hello "+z.select("day", [("1","mon"),
</div>
</div>
<img src="/assets/themes/zeppelin/img/screenshots/form_select_prog.png" />
<img src="../assets/themes/zeppelin/img/screenshots/form_select_prog.png" />
#### Checkbox form
<div class="codetabs">
@ -166,4 +166,4 @@ print("Hello "+ " and ".join(z.checkbox("fruit", options, ["apple"])))
</div>
</div>
<img src="/assets/themes/zeppelin/img/screenshots/form_checkbox_prog.png" />
<img src="../assets/themes/zeppelin/img/screenshots/form_checkbox_prog.png" />

View file

@ -88,7 +88,7 @@ you need to do is use our %angular support.
<i style="font-size: 15px;" class="icon-notebook"></i> Create new note</a></h5>
<ul style="list-style-type: none;">
<li ng-repeat="note in home.notes.list track by $index"><i style="font-size: 10px;" class="icon-doc"></i>
<a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}}</a>
<a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{noteName(note)}}</a>
</li>
</ul>
</div>

View file

@ -34,11 +34,12 @@ This instruction based on Ubuntu 14.04 LTS but may work with other OS with few c
1. Install NGINX server on your server instance
You can install NGINX server with same machine where zeppelin installed or separate machine where it is dedicated to serve as proxy server.
You can install NGINX server with same box where zeppelin installed or separate box where it is dedicated to serve as proxy server.
```
$ apt-get install nginx
```
*Important: On pre 1.3.13 version of NGINX, Proxy for Websocket may not fully works. Please use latest version of NGINX. See: [NGINX documentation](https://www.nginx.com/blog/websocket-nginx/)*
1. Setup init script in NGINX
@ -46,30 +47,30 @@ This instruction based on Ubuntu 14.04 LTS but may work with other OS with few c
```
$ cd /etc/nginx/sites-available
$ touch my-basic-auth
$ touch my-zeppelin-auth-setting
```
Now add this script into `my-basic-auth` file. You can comment out `optional` lines If you want serve Zeppelin under regular HTTP 80 Port.
Now add this script into `my-zeppelin-auth-setting` file. You can comment out `optional` lines If you want serve Zeppelin under regular HTTP 80 Port.
```
upstream zeppelin {
server [YOUR-ZEPPELIN-SERVER-IP]:8080;
server [YOUR-ZEPPELIN-SERVER-IP]:[YOUR-ZEPPELIN-SERVER-PORT]; # For security, It is highly recommended to make this address/port as non-public accessible
}
# Zeppelin Website
server {
listen [YOUR-ZEPPELIN-WEB-SERVER-PORT];
listen 443 ssl; # optional, to serve HTTPS connection
server_name [YOUR-ZEPPELIN-SERVER-HOST]; # for example: zeppelin.mycompany.com
listen 443 ssl; # optional, to serve HTTPS connection
server_name [YOUR-ZEPPELIN-SERVER-HOST]; # for example: zeppelin.mycompany.com
ssl_certificate [PATH-TO-YOUR-CERT-FILE]; # optional, to serve HTTPS connection
ssl_certificate_key [PATH-TO-YOUR-CERT-KEY-FILE]; # optional, to serve HTTPS connection
if ($ssl_protocol = "") {
rewrite ^ https://$host$request_uri? permanent; # optional, force to use HTTPS
rewrite ^ https://$host$request_uri? permanent; # optional, to force use of HTTPS
}
location / {
location / { # For regular websever support
proxy_pass http://zeppelin;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -80,7 +81,7 @@ This instruction based on Ubuntu 14.04 LTS but may work with other OS with few c
auth_basic_user_file /etc/nginx/.htpasswd;
}
location /ws {
location /ws { # For websocket support
proxy_pass http://zeppelin;
proxy_http_version 1.1;
proxy_set_header Upgrade websocket;
@ -93,7 +94,7 @@ This instruction based on Ubuntu 14.04 LTS but may work with other OS with few c
Then make a symbolic link to this file from `/etc/nginx/sites-enabled/` to enable configuration above when NGINX reloads.
```
$ ln -s /etc/nginx/sites-enabled/my-basic-auth /etc/nginx/sites-available/my-basic-auth
$ ln -s /etc/nginx/sites-enabled/my-zeppelin-auth-setting /etc/nginx/sites-available/my-zeppelin-auth-setting
```
1. Setup user credential into `.htpasswd` file and restart server
@ -102,11 +103,11 @@ This instruction based on Ubuntu 14.04 LTS but may work with other OS with few c
```
$ cd /etc/nginx
$ htpasswd -c htpasswd [YOUR_ID]
$ NEW passwd: [YOUR_PASSWORD]
$ RE-type new passwd: [YOUR_PASSWORD_AGAIN]
$ htpasswd -c htpasswd [YOUR-ID]
$ NEW passwd: [YOUR-PASSWORD]
$ RE-type new passwd: [YOUR-PASSWORD-AGAIN]
```
Or you can use your own apache `.htpasswd` files in other location by setup property `auth_basic_user_file`
Or you can use your own apache `.htpasswd` files in other location for setting up property: `auth_basic_user_file`
Restart NGINX server.
@ -115,9 +116,6 @@ This instruction based on Ubuntu 14.04 LTS but may work with other OS with few c
```
Then check HTTP Basic Authentication works in browser. If you can see regular basic auth popup and then able to login with credential you entered into `.htpasswd` you are good to go.
<img src="/assets/themes/zeppelin/img/screenshots/authentication-basic-auth-nginx-request.png" />
<img src="/assets/themes/zeppelin/img/screenshots/authentication-basic-auth-nginx-https.png" />
1. More security consideration
* Using HTTPS connection with Basic Authentication is highly recommended since basic auth without encryption may expose your important credential information over the network.

View file

@ -0,0 +1,139 @@
---
layout: page
title: "Zeppelin UI Layout"
description: "Description of Zeppelin UI Layout"
group: ui_layout
---
<!--
Licensed 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.
-->
## Home Page
The first time you connect to Zeppelin, you'll land at the main page similar to the below screen capture
<img src="/assets/themes/zeppelin/img/ui-img/homepage.png" />
On the left of the page are listed all existing notes. Those notes are stored by default in the `$ZEPPELIN_HOME/notebook` folder.
You can filter them by name using the input text form. You can also create an new note, refresh the list of existing notes
(in case you manually copy them into the `$ZEPPELIN_HOME/notebook` folder) and import a note
<img src="/assets/themes/zeppelin/img/ui-img/notes_management.png" />
When clicking on `Import Note` link, a new dialog open. From there you can import your note from local disk or from a remote location
if you provide the URL.
<img src="/assets/themes/zeppelin/img/ui-img/note_import_dialog.png" />
By default, the name of the imported note is the same as the original note but you can override it by providing a new name
<br />
## Menus
### 1. Notebook
The `Notebook` menu proposes almost the same features as the note management section in the home page. From the drop-down menu you can:
1. Open a selected note
2. Filter node by name
3. Create a new note
<img src="../assets/themes/zeppelin/img/ui-img/notebook_menu.png" />
### 2. Interpreter
In this menu you can:
1. Configure existing **interpreter instance**
2. Add/remove **interpreter instances**
<img src="../assets/themes/zeppelin/img/ui-img/interpreter_menu.png" />
### 3. Configuration
This menu displays all the Zeppelin configuration that are set in the config file `$ZEPPELIN_HOME/conf/zeppelin-site.xml`
<img src="../assets/themes/zeppelin/img/ui-img/configuration_menu.png" />
<br />
## Note Layout
Each Zeppelin note is composed of 1 .. N paragraphs. The note can be viewed as a paragraph container.
<img src="../assets/themes/zeppelin/img/ui-img/note_paragraph_layout.png" />
### Paragraph
Each paragraph consists of 2 sections: `code section` where you put your source code and `result section` where you can see the result of the code execution.
<img src="../assets/themes/zeppelin/img/ui-img/paragraph_layout.png" />
On the top-right corner of each paragraph there are some commands to:
* execute the paragraph code
* hide/show `code section`
* hide/show `result section`
* configure the paragraph
To configure the paragraph, just click on the gear icon:
<img src="../assets/themes/zeppelin/img/ui-img/paragraph_configuration_dialog.png" />
From this dialog, you can (in descending order):
* find the **paragraph id** ( **20150924-163507_134879501** )
* control paragraph width. Since Zeppelin is using the grid system of **Twitter Bootstrap**, each paragraph width can be changed from 1 to 12
* move the paragraph 1 level up
* move the paragraph 1 level down
* create a new paragraph
* change paragraph title
* show/hide line number in the `code section`
* disable the run button for this paragraph
* export the current paragraph as an **iframe** and open the **iframe** in a new window
* clear the `result section`
* delete the current paragraph
### Note toolbar
At the top of the note, you can find a toolbar which exposes command buttons as well as configuration, security and display options
<img src="../assets/themes/zeppelin/img/ui-img/note_toolbar.png" />
On the far right is displayed the note name, just click on it to reveal the input form and update it
In the middle of the toolbar you can find the command buttons:
* execute all the paragraphs **sequentially**, in their display order
* hide/show `code section` of all paragraphs
* hide/show `result section` of all paragraphs
* clear the `result section` of all paragraphs
* clone the current note
* export the current note to a JSON file. _Please note that the `code section` and `result section` of all paragraphs will be exported. If you have heavy data in the `result section` of some paragraphs, it is recommended to clean them before exporting
* commit the current node content
* delete the note
* schedule the execution of **all paragraph** using a CRON syntax
<img src="../assets/themes/zeppelin/img/ui-img/note_commands.png" />
On the right of the note tool bar you can find configuration icons:
* display all the keyboard shorcuts
* configure the interpreters binding to the current note
* configure the note permissions
* switch the node display mode between `default`, `simple` and `report`
<img src="../assets/themes/zeppelin/img/ui-img/note_configuration.png" />

View file

@ -22,8 +22,8 @@ import org.apache.commons.io.IOUtils;
import org.apache.zeppelin.interpreter.InterpreterException;
import org.apache.zeppelin.interpreter.InterpreterOutput;
import org.apache.zeppelin.interpreter.InterpreterOutputListener;
import parquet.org.slf4j.Logger;
import parquet.org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.Collections;

View file

@ -83,14 +83,14 @@ class PyZeppelinContext(dict):
def select(self, name, options, defaultValue = ""):
# auto_convert to ArrayList doesn't match the method signature on JVM side
tuples = map(lambda items: self.__tupleToScalaTuple2(items), options)
tuples = list(map(lambda items: self.__tupleToScalaTuple2(items), options))
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)
defaultChecked = list(map(lambda items: items[0], options))
optionTuples = list(map(lambda items: self.__tupleToScalaTuple2(items), options))
optionIterables = gateway.jvm.scala.collection.JavaConversions.collectionAsScalaIterable(optionTuples)
defaultCheckedIterables = gateway.jvm.scala.collection.JavaConversions.collectionAsScalaIterable(defaultChecked)

View file

@ -132,6 +132,10 @@ The text of each license is also included at licenses/LICENSE-[project]-[version
(The MIT License) lodash v3.9.3 (https://lodash.com/) - https://github.com/lodash/lodash/blob/3.9.3/LICENSE.txt
(The MIT License) angular-filter v0.5.4 (https://github.com/a8m/angular-filter) - https://github.com/a8m/angular-filter/blob/v0.5.4/license.md
(The MIT License) ngToast v1.5.5 (http://tamerayd.in/ngToast/) - http://tameraydin.mit-license.org/
(The MIT License) Handsontable v0.24.2 (https://github.com/handsontable/handsontable) - https://github.com/handsontable/handsontable/blob/master/LICENSE
(The MIT License) Zeroclipboard v2.2.0 (https://github.com/zeroclipboard/zeroclipboard) - https://github.com/zeroclipboard/zeroclipboard/blob/v2.2.0/LICENSE
(The MIT License) Moment v2.9.0 (https://github.com/moment/moment) - https://github.com/moment/moment/blob/2.9.0/LICENSE
(The MIT License) Pikaday v1.3.2 (https://github.com/dbushell/Pikaday) - https://github.com/dbushell/Pikaday/blob/1.3.2/LICENSE
(The MIT License) slf4j v1.7.10 (org.slf4j:slf4j-api:jar:1.7.10 - http://www.slf4j.org) - http://www.slf4j.org/license.html
(The MIT License) slf4j-log4j12 v1.7.10 (org.slf4j:slf4j-log4j12:jar:1.7.10 - http://www.slf4j.org) - http://www.slf4j.org/license.html
(The MIT License) bcprov-jdk15on v1.51 (org.bouncycastle:bcprov-jdk15on:jar:1.51 - http://www.bouncycastle.org/java.html) - http://www.bouncycastle.org/licence.html

View file

@ -65,6 +65,9 @@ public class LoginRestApi {
JsonResponse response = null;
// ticket set to anonymous for anonymous user. Simplify testing.
Subject currentUser = org.apache.shiro.SecurityUtils.getSubject();
if (currentUser.isAuthenticated()) {
currentUser.logout();
}
if (!currentUser.isAuthenticated()) {
try {
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
@ -107,6 +110,23 @@ public class LoginRestApi {
LOG.warn(response.toString());
return response.build();
}
@POST
@Path("logout")
public Response logout() {
JsonResponse response;
Subject currentUser = org.apache.shiro.SecurityUtils.getSubject();
currentUser.logout();
Map<String, String> data = new HashMap<>();
data.put("principal", "anonymous");
data.put("roles", "");
data.put("ticket", "anonymous");
response = new JsonResponse(Response.Status.OK, "", data);
LOG.warn(response.toString());
return response.build();
}
}

View file

@ -665,14 +665,29 @@ public class NotebookRestApi {
}
/**
* Search for a Notes
* Search for a Notes with permissions
*/
@GET
@Path("search")
public Response search(@QueryParam("q") String queryTerm) {
LOG.info("Searching notebooks for: {}", queryTerm);
String principal = SecurityUtils.getPrincipal();
HashSet<String> roles = SecurityUtils.getRoles();
HashSet<String> userAndRoles = new HashSet<String>();
userAndRoles.add(principal);
userAndRoles.addAll(roles);
List<Map<String, String>> notebooksFound = notebookIndex.query(queryTerm);
LOG.info("{} notbooks found", notebooksFound.size());
for (int i = 0; i < notebooksFound.size(); i++) {
String[] Id = notebooksFound.get(i).get("id").split("/", 2);
String noteId = Id[0];
if (!notebookAuthorization.isOwner(noteId, userAndRoles) &&
!notebookAuthorization.isReader(noteId, userAndRoles) &&
!notebookAuthorization.isWriter(noteId, userAndRoles)) {
notebooksFound.remove(i);
i--;
}
}
LOG.info("{} notebooks found", notebooksFound.size());
return new JsonResponse<>(Status.OK, notebooksFound).build();
}

View file

@ -255,7 +255,7 @@ public class ZeppelinServer extends Application {
webapp.setInitParameter("shiroConfigLocations",
new File(conf.getShiroPath()).toURI().toString());
webapp.addFilter(org.apache.shiro.web.servlet.ShiroFilter.class, "/*",
webapp.addFilter(org.apache.shiro.web.servlet.ShiroFilter.class, "/api/*",
EnumSet.allOf(DispatcherType.class));
webapp.addEventListener(new org.apache.shiro.web.env.EnvironmentLoaderListener());

View file

@ -135,7 +135,7 @@ public class NotebookServer extends WebSocketServlet implements
/** Lets be elegant here */
switch (messagereceived.op) {
case LIST_NOTES:
broadcastNoteList();
unicastNoteList(conn);
break;
case RELOAD_NOTES_FROM_REPO:
broadcastReloadedNoteList();
@ -203,7 +203,6 @@ public class NotebookServer extends WebSocketServlet implements
checkpointNotebook(conn, notebook, messagereceived);
break;
default:
broadcastNoteList();
break;
}
} catch (Exception e) {
@ -342,6 +341,14 @@ public class NotebookServer extends WebSocketServlet implements
}
}
private void unicast(Message m, NotebookSocket conn) {
try {
conn.send(serializeMessage(m));
} catch (IOException e) {
LOG.error("socket error", e);
}
}
public List<Map<String, String>> generateNotebooksInfo(boolean needsReload) {
Notebook notebook = notebook();
@ -384,19 +391,27 @@ public class NotebookServer extends WebSocketServlet implements
broadcastAll(new Message(OP.NOTES_INFO).put("notes", notesInfo));
}
public void unicastNoteList(NotebookSocket conn) {
List<Map<String, String>> notesInfo = generateNotebooksInfo(false);
unicast(new Message(OP.NOTES_INFO).put("notes", notesInfo), conn);
}
public void broadcastReloadedNoteList() {
List<Map<String, String>> notesInfo = generateNotebooksInfo(true);
broadcastAll(new Message(OP.NOTES_INFO).put("notes", notesInfo));
}
void permissionError(NotebookSocket conn, String op, Set<String> current,
Set<String> allowed) throws IOException {
void permissionError(NotebookSocket conn, String op, Set<String> userAndRoles,
Set<String> allowed) throws IOException {
LOG.info("Cannot {}. Connection readers {}. Allowed readers {}",
op, current, allowed);
op, userAndRoles, allowed);
String userName = userAndRoles.iterator().next();
conn.send(serializeMessage(new Message(OP.AUTH_INFO).put("info",
"Insufficient privileges to " + op + " note.\n\n" +
"Insufficient privileges to " + op + " notebook.\n\n" +
"Allowed users or roles: " + allowed.toString() + "\n\n" +
"User belongs to: " + current.toString())));
"But the user " + userName + " belongs to: " + userAndRoles.toString())));
}
private void sendNote(NotebookSocket conn, HashSet<String> userAndRoles, Notebook notebook,
@ -417,7 +432,6 @@ public class NotebookServer extends WebSocketServlet implements
if (note != null) {
if (!notebookAuthorization.isReader(noteId, userAndRoles)) {
permissionError(conn, "read", userAndRoles, notebookAuthorization.getReaders(noteId));
broadcastNoteList();
return;
}
addConnectionToNote(note.id(), conn);
@ -439,7 +453,6 @@ public class NotebookServer extends WebSocketServlet implements
NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
if (!notebookAuthorization.isReader(noteId, userAndRoles)) {
permissionError(conn, "read", userAndRoles, notebookAuthorization.getReaders(noteId));
broadcastNoteList();
return;
}
addConnectionToNote(note.id(), conn);
@ -465,6 +478,12 @@ public class NotebookServer extends WebSocketServlet implements
return;
}
NotebookAuthorization notebookAuthorization = notebook.getNotebookAuthorization();
if (!notebookAuthorization.isWriter(noteId, userAndRoles)) {
permissionError(conn, "update", userAndRoles, notebookAuthorization.getWriters(noteId));
return;
}
Note note = notebook.getNote(noteId);
if (note != null) {
boolean cronUpdated = isCronUpdated(config, note.getConfig());
@ -990,6 +1009,7 @@ public class NotebookServer extends WebSocketServlet implements
new InterpreterResult(InterpreterResult.Code.ERROR, ex.getMessage()),
ex);
p.setStatus(Status.ERROR);
broadcast(note.id(), new Message(OP.PARAGRAPH).put("paragraph", p));
}
}
}

View file

@ -167,9 +167,8 @@ public class SparkParagraphIT extends AbstractZeppelinIT {
WebElement paragraph1Result = driver.findElement(By.xpath(
getParagraphXPath(1) + "//div[@class=\"tableDisplay\"]"));
collector.checkThat("Paragraph from SparkParagraphIT of testSqlSpark result: ",
paragraph1Result.getText().toString(), CoreMatchers.equalTo("age job marital education balance\n" +
"30 unemployed married primary 1,787")
);
paragraph1Result.getText().toString(), CoreMatchers.equalTo("age\njob\nmarital\neducation\nbalance\n30" +
" unemployed married primary 1,787\nage\njob\nmarital\neducation\nbalance"));
} catch (Exception e) {
handleException("Exception in SparkParagraphIT while testSqlSpark", e);
}

View file

@ -18,6 +18,7 @@
package org.apache.zeppelin.rest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -690,5 +691,96 @@ public class ZeppelinRestApiTest extends AbstractTestRestApi {
ZeppelinServer.notebook.removeNote(note.getId());
}
@Test
public void testSearch() throws IOException {
Map<String, String> body;
GetMethod getSecurityTicket = httpGet("/security/ticket");
getSecurityTicket.addRequestHeader("Origin", "http://localhost");
Map<String, Object> respSecurityTicket = gson.fromJson(getSecurityTicket.getResponseBodyAsString(),
new TypeToken<Map<String, Object>>() {
}.getType());
body = (Map<String, String>) respSecurityTicket.get("body");
String username = body.get("principal");
getSecurityTicket.releaseConnection();
Note note1 = ZeppelinServer.notebook.createNote();
String jsonRequest = "{\"title\": \"title1\", \"text\": \"ThisIsToTestSearchMethodWithPermissions 1\"}";
PostMethod postNotebookText = httpPost("/notebook/" + note1.getId() + "/paragraph", jsonRequest);
postNotebookText.releaseConnection();
Note note2 = ZeppelinServer.notebook.createNote();
jsonRequest = "{\"title\": \"title1\", \"text\": \"ThisIsToTestSearchMethodWithPermissions 2\"}";
postNotebookText = httpPost("/notebook/" + note2.getId() + "/paragraph", jsonRequest);
postNotebookText.releaseConnection();
String jsonPermissions = "{\"owners\":[\"" + username + "\"],\"readers\":[\"" + username + "\"],\"writers\":[\"" + username + "\"]}";
PutMethod putPermission = httpPut("/notebook/" + note1.getId() + "/permissions", jsonPermissions);
putPermission.releaseConnection();
jsonPermissions = "{\"owners\":[\"admin\"],\"readers\":[\"admin\"],\"writers\":[\"admin\"]}";
putPermission = httpPut("/notebook/" + note2.getId() + "/permissions", jsonPermissions);
putPermission.releaseConnection();
GetMethod searchNotebook = httpGet("/notebook/search?q='ThisIsToTestSearchMethodWithPermissions'");
searchNotebook.addRequestHeader("Origin", "http://localhost");
Map<String, Object> respSearchResult = gson.fromJson(searchNotebook.getResponseBodyAsString(),
new TypeToken<Map<String, Object>>() {
}.getType());
ArrayList searchBody = (ArrayList) respSearchResult.get("body");
assertEquals("At-least one search results is there", true, searchBody.size() >= 1);
for (int i = 0; i < searchBody.size(); i++) {
Map<String, String> searchResult = (Map<String, String>) searchBody.get(i);
String userId = searchResult.get("id").split("/", 2)[0];
GetMethod getPermission = httpGet("/notebook/" + userId + "/permissions");
getPermission.addRequestHeader("Origin", "http://localhost");
Map<String, Object> resp = gson.fromJson(getPermission.getResponseBodyAsString(),
new TypeToken<Map<String, Object>>() {
}.getType());
Map<String, ArrayList> permissions = (Map<String, ArrayList>) resp.get("body");
ArrayList owners = permissions.get("owners");
ArrayList readers = permissions.get("readers");
ArrayList writers = permissions.get("writers");
if (owners.size() != 0 && readers.size() != 0 && writers.size() != 0) {
assertEquals("User has permissions ", true, (owners.contains(username) || readers.contains(username) ||
writers.contains(username)));
}
getPermission.releaseConnection();
}
searchNotebook.releaseConnection();
ZeppelinServer.notebook.removeNote(note1.getId());
ZeppelinServer.notebook.removeNote(note2.getId());
}
@Test
public void testTitleSearch() throws IOException {
Note note = ZeppelinServer.notebook.createNote();
String jsonRequest = "{\"title\": \"testTitleSearchOfParagraph\", \"text\": \"ThisIsToTestSearchMethodWithTitle \"}";
PostMethod postNotebookText = httpPost("/notebook/" + note.getId() + "/paragraph", jsonRequest);
postNotebookText.releaseConnection();
GetMethod searchNotebook = httpGet("/notebook/search?q='testTitleSearchOfParagraph'");
searchNotebook.addRequestHeader("Origin", "http://localhost");
Map<String, Object> respSearchResult = gson.fromJson(searchNotebook.getResponseBodyAsString(),
new TypeToken<Map<String, Object>>() {
}.getType());
ArrayList searchBody = (ArrayList) respSearchResult.get("body");
int numberOfTitleHits = 0;
for (int i = 0; i < searchBody.size(); i++) {
Map<String, String> searchResult = (Map<String, String>) searchBody.get(i);
if (searchResult.get("header").contains("testTitleSearchOfParagraph")) {
numberOfTitleHits++;
}
}
assertEquals("Paragraph title hits must be at-least one", true, numberOfTitleHits >= 1);
searchNotebook.releaseConnection();
ZeppelinServer.notebook.removeNote(note.getId());
}
}

View file

@ -31,6 +31,7 @@
"nv": false,
"ace": false,
"d3": false,
"BootstrapDialog": false
"BootstrapDialog": false,
"Handsontable": false
}
}

View file

@ -24,15 +24,13 @@
"angular-elastic": "~2.4.2",
"angular-elastic-input": "~2.2.0",
"angular-xeditable": "0.1.8",
"highlightjs": "~8.4.0",
"highlightjs": "^9.2.0",
"lodash": "~3.9.3",
"angular-filter": "~0.5.4",
"ngtoast": "~2.0.0",
"ng-focus-if": "~1.0.2",
"bootstrap3-dialog": "bootstrap-dialog#~1.34.7",
"floatThead": "~1.3.2",
"datatables.net-bs": "~1.10.11",
"datatables.net-buttons-bs": "~1.1.2"
"handsontable": "~0.24.2"
},
"devDependencies": {
"angular-mocks": "1.5.0"

View file

@ -71,4 +71,13 @@ angular.module('zeppelinWebApp').controller('HomeCtrl', function($scope, noteboo
websocketMsgSrv.reloadAllNotesFromRepo();
$scope.isReloadingNotes = true;
};
$scope.toggleFolderNode = function(node) {
node.hidden = !node.hidden;
};
$rootScope.noteName = function(note) {
return arrayOrderingSrv.getNoteName(note);
};
});

View file

@ -151,6 +151,70 @@ a.navbar-brand:hover {
font: normal normal normal 14px/1 FontAwesome;
}
.dropdown-submenu {
position: relative;
}
.dropdown-submenu a {
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
text-decoration: none;
}
.dropdown-submenu a:hover {
background-color: #f5f5f5;
}
.dropdown-submenu > .dropdown-menu {
top:0;
left:100%;
margin-top:-6px;
margin-left:-1px;
-webkit-border-radius:0 6px 6px 6px;
-moz-border-radius:0 6px 6px 6px;
border-radius:0 6px 6px 6px;
}
.dropdown-submenu:hover > .dropdown-menu {
display:block;
}
/* overwrite the style of the first element of dropdown-menu */
.dropdown-submenu:hover > .dropdown-menu > li:first-child > a:hover {
color: #262626;
text-decoration: none;
background: #f5f5f5;
}
.dropdown-submenu > a:after {
display:block;
content:" ";
float:right;
width:0;
height:0;
border-color:transparent;
border-style:solid;
border-width:5px 0 5px 5px;
border-left-color:#cccccc;
margin-top:5px;
margin-right:-10px;
}
.dropdown-submenu:hover > a:after {
border-left-color:#ffffff;
}
.dropdown-submenu.pull-left {
float:none;
}
.dropdown-submenu.pull-left > .dropdown-menu {
left:-100%;
margin-left:10px;
-webkit-border-radius:6px 0 6px 6px;
-moz-border-radius:6px 0 6px 6px;
border-radius:6px 0 6px 6px;
}
@media (max-width: 767px) {
.navbar-inverse .navbar-nav .open .dropdown-menu > li > a {
color: #D3D3D3;
@ -199,6 +263,7 @@ a.navbar-brand:hover {
#notebook-list {
position: relative;
overflow: hidden;
display: inline;
}
@media (min-width: 768px) {

View file

@ -12,6 +12,24 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<script type="text/ng-template" id="notebook_folder_renderer.html">
<div ng-if="node.children == null">
<a style="text-decoration: none;" href="#/notebook/{{node.id}}">
<i style="font-size: 10px;" class="icon-doc"/> {{noteName(node)}}
</a>
</div>
<div ng-if="node.children != null">
<a style="text-decoration: none; cursor: pointer;" ng-click="toggleFolderNode(node)">
<i style="font-size: 10px;" ng-class="node.hidden ? 'icon-folder' : 'icon-folder-alt'" /> {{noteName(node)}}
</a>
<div ng-if="!node.hidden">
<ul style="list-style-type: none; padding-left:15px;">
<li ng-repeat="node in node.children" ng-include="'notebook_folder_renderer.html'" />
</ul>
</div>
</div>
</script>
<div ng-controller="HomeCtrl as home">
<div ng-show="home.staticHome" class="box width-full home">
<div class="zeppelin">
@ -25,7 +43,7 @@ limitations under the License.
You can make beautiful data-driven, interactive, collaborative document with SQL, code and even more!<br>
<div class="row">
<div class="col-md-4">
<div class="col-md-4" ng-if="ticket">
<h4>Notebook
<i ng-class="isReloadingNotes ? 'fa fa-refresh fa-spin' : 'fa fa-refresh'"
ng-style="!isReloadingNotes && {'cursor': 'pointer'}" style="font-size: 13px;"
@ -43,8 +61,17 @@ limitations under the License.
<li class="filter-names" ng-include="'components/filterNoteNames/filter-note-names.html'"></li>
<li ng-repeat="note in home.notes.list | filter:query | orderBy:home.arrayOrderingSrv.notebookListOrdering track by $index">
<i style="font-size: 10px;" class="icon-doc"></i>
<a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}}</a>
<a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{noteName(note)}}</a>
</li>
<div ng-if="!query || query.name === ''">
<li ng-repeat="node in home.notes.root.children | orderBy:home.arrayOrderingSrv.notebookListOrdering track by $index" ng-include="'notebook_folder_renderer.html'" />
</div>
<div ng-if="query && query.name !== ''">
<li ng-repeat="note in home.notes.flatList | filter:query | orderBy:home.arrayOrderingSrv.notebookListOrdering track by $index">
<i style="font-size: 10px;" class="icon-doc"></i>
<a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{noteName(note)}}</a>
</li>
</div>
</ul>
</div>
</div>

View file

@ -13,9 +13,9 @@ limitations under the License.
-->
<div class="noteAction" ng-show="note.id && !paragraphUrl">
<h3>
<input type="text" pu-elastic-input class="form-control2" placeholder="{{note.name || 'Note ' + note.id}}" style="min-width: 200px; max-width: 600px;"
<input type="text" pu-elastic-input class="form-control2" placeholder="{{noteName(note)}}" style="min-width: 200px; max-width: 600px;"
ng-show="showEditor" ng-model="note.name" ng-blur="sendNewName();showEditor = false;" ng-enter="sendNewName();showEditor = false;" ng-escape="note.name = oldName; showEditor = false" focus-if="showEditor" />
<p class="form-control-static2" ng-click="showEditor = true; oldName = note.name" ng-show="!showEditor">{{note.name || 'Note ' + note.id}}</p>
<p class="form-control-static2" ng-click="showEditor = true; oldName = note.name" ng-show="!showEditor">{{noteName(note)}}</p>
<span class="labelBtn btn-group">
<button type="button"
class="btn btn-default btn-xs"

View file

@ -686,10 +686,24 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
}).
error(function(data, status, headers, config) {
console.log('Error %o %o', status, data.message);
BootstrapDialog.alert({
closable: true,
title: 'Insufficient privileges',
message: data.message
BootstrapDialog.show({
closable: true,
title: 'Insufficient privileges',
message: data.message,
buttons: [{
label: 'Login',
action: function(dialog) {
dialog.close();
angular.element('#loginModal').modal({
show: 'true'
});
}
}, {
label: 'Cancel',
action: function(dialog){
dialog.close();
}
}]
});
});
};

View file

@ -411,6 +411,7 @@ angular.module('zeppelinWebApp')
var resultRefreshed = (data.paragraph.dateFinished !== $scope.paragraph.dateFinished) ||
isEmpty(data.paragraph.result) !== isEmpty($scope.paragraph.result) ||
data.paragraph.status === 'ERROR' ||
(!newActiveApp && oldActiveApp !== newActiveApp);
var statusChanged = (data.paragraph.status !== $scope.paragraph.status);
@ -434,6 +435,7 @@ angular.module('zeppelinWebApp')
}
/** push the rest */
$scope.paragraph.authenticationInfo = data.paragraph.authenticationInfo;
$scope.paragraph.aborted = data.paragraph.aborted;
$scope.paragraph.dateUpdated = data.paragraph.dateUpdated;
$scope.paragraph.dateCreated = data.paragraph.dateCreated;
@ -1023,7 +1025,13 @@ angular.module('zeppelinWebApp')
}
return '';
}
var desc = 'Took ' + (timeMs/1000) + ' seconds';
var user = 'anonymous';
var authInfo = pdata.authenticationInfo;
if (authInfo && authInfo.user) {
user = pdata.authenticationInfo.user;
}
var dateUpdated = (pdata.dateUpdated === null) ? 'unknown' : pdata.dateUpdated;
var desc = 'Took ' + (timeMs/1000) + ' seconds. Last updated by ' + user + ' at time ' + dateUpdated + '.';
if ($scope.isResultOutdated()){
desc += ' (outdated)';
}
@ -1271,110 +1279,42 @@ angular.module('zeppelinWebApp')
};
var setTable = function(type, data, refresh) {
var getTableContentFormat = function(d) {
if (isNaN(d)) {
if (d.length>'%html'.length && '%html ' === d.substring(0, '%html '.length)) {
return 'html';
} else {
return '';
}
} else {
return '';
}
};
var formatTableContent = function(d) {
if (isNaN(d)) {
var f = getTableContentFormat(d);
if (f !== '') {
return d.substring(f.length+2);
} else {
return d;
}
} else {
var dStr = d.toString();
var splitted = dStr.split('.');
var formatted = splitted[0].replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
if (splitted.length>1) {
formatted+= '.'+splitted[1];
}
return formatted;
}
};
var renderTable = function() {
var html = '';
html += '<table class="table table-hover table-condensed">';
html += ' <thead>';
html += ' <tr style="background-color: #F6F6F6; font-weight: bold;">';
for (var titleIndex in $scope.paragraph.result.columnNames) {
html += '<th>'+$scope.paragraph.result.columnNames[titleIndex].name+'</th>';
}
html += ' </tr>';
html += ' </thead>';
html += ' <tbody>';
for (var r in $scope.paragraph.result.msgTable) {
var row = $scope.paragraph.result.msgTable[r];
html += ' <tr>';
for (var index in row) {
var v = row[index].value;
if (getTableContentFormat(v) !== 'html') {
v = v.replace(/[\u00A0-\u9999<>\&]/gim, function(i) {
return '&#'+i.charCodeAt(0)+';';
});
}
html += ' <td>'+formatTableContent(v)+'</td>';
var height = $scope.paragraph.config.graph.height;
angular.element('#p' + $scope.paragraph.id + '_table').css('height', height);
var resultRows = $scope.paragraph.result.rows;
var columnNames = _.pluck($scope.paragraph.result.columnNames, 'name');
var container = document.getElementById('p' + $scope.paragraph.id + '_table');
var handsontable = new Handsontable(container, {
data: resultRows,
colHeaders: columnNames,
rowHeaders: false,
stretchH: 'all',
sortIndicator: true,
columnSorting: true,
contextMenu: false,
manualColumnResize: true,
manualRowResize: true,
editor: false,
fillHandle: false,
disableVisualSelection: true,
cells: function (row, col, prop) {
var cellProperties = {};
cellProperties.renderer = function(instance, td, row, col, prop, value, cellProperties) {
Handsontable.NumericCell.renderer.apply(this, arguments);
if (!isNaN(value)) {
cellProperties.type = 'numeric';
cellProperties.format = '0,0';
cellProperties.editor = false;
td.style.textAlign = 'left';
} else if (value.length > '%html'.length && '%html ' === value.substring(0, '%html '.length)) {
td.innerHTML = value.substring('%html'.length);
}
};
return cellProperties;
}
html += ' </tr>';
}
html += ' </tbody>';
html += '</table>';
var tableDomEl = angular.element('#p' + $scope.paragraph.id + '_table');
tableDomEl.html(html);
var oTable = tableDomEl.children(1).DataTable({
paging: false,
info: false,
autoWidth: false,
lengthChange: false,
searching: false,
dom: '<>'
});
if ($scope.paragraph.result.msgTable.length > 10000) {
tableDomEl.css({
'overflow': 'scroll',
'height': $scope.paragraph.config.graph.height
});
} else {
var dataTable = angular.element('#p' + $scope.paragraph.id + '_table .table');
dataTable.floatThead({
scrollContainer: function(dataTable) {
return tableDomEl;
}
});
dataTable.on('remove', function () {
dataTable.floatThead('destroy');
});
tableDomEl.css({
'position': 'relative',
'height': '100%'
});
tableDomEl.perfectScrollbar('destroy')
.perfectScrollbar({minScrollbarLength: 20});
angular.element('.ps-scrollbar-y-rail').css('z-index', '1002');
// set table height
var psHeight = $scope.paragraph.config.graph.height;
tableDomEl.css('height', psHeight);
tableDomEl.perfectScrollbar('update');
}
};
var retryRenderer = function() {

View file

@ -354,6 +354,7 @@ table.dataTable.table-condensed .sorting_desc:after {
.tableDisplay .option .columns ul {
background: white;
overflow: auto;
width: auto;
padding: 3px 3px 3px 3px;
height: 150px;
@ -396,6 +397,29 @@ table.dataTable.table-condensed .sorting_desc:after {
background: none;
}
/*
Handsontable
*/
.handsontable th {
font-weight: bold;
}
.handsontable th, .handsontable td {
border-right: 0px;
border-left: 0px !important;
padding: 4px;
}
.handsontable tr:first-child th {
text-align: left;
border-top: 0px;
padding: 4px 0px 0px 0px;
border-left: 0px;
border-right: 0px;
border-bottom: 2px solid #CCC;
}
/*
Pivot CSS
*/

View file

@ -74,12 +74,20 @@ angular
};
}
var lines = note.snippet
var result = '';
if (note.header !== '') {
result = note.header + '\n\n' + note.snippet;
} else {
result = note.snippet;
}
var lines = result
.split('\n')
.map(function(line, row) {
var match = line.match(/<B>(.+?)<\/B>/);
// return early if nothing to highlight
// return early if nothing to highlight
if (!match) {
return line;
}
@ -93,15 +101,31 @@ angular
indeces.forEach(function(start) {
var end = start + term.length;
_editor
.getSession()
.addMarker(
if (note.header !== '' && row === 0) {
_editor
.getSession()
.addMarker(
new Range(row, 0, row, line.length),
'search-results-highlight-header',
'background'
);
_editor
.getSession()
.addMarker(
new Range(row, start, row, end),
'search-results-highlight',
'line'
);
} else {
_editor
.getSession()
.addMarker(
new Range(row, start, row, end),
'search-results-highlight',
'line'
);
}
});
return __line;
});

View file

@ -21,7 +21,7 @@ limitations under the License.
<i style="font-size: 10px;" class="icon-doc"></i>
<a class="search-results-header"
href="#/notebook/{{note.id}}">
{{note.name || 'Note ' + note.id}}
{{note.name.trim()==='' && 'Note ' + note.id.split('/',2)[0] || note.name}}
</a>
</h4>
</div>

View file

@ -31,6 +31,11 @@
position: absolute;
}
.search-results-highlight-header {
background-color: #e6f2ff;
position: absolute;
}
/* remove error highlighting */
.search-results .ace_invalid {
background: none !important;

View file

@ -15,8 +15,18 @@
angular.module('zeppelinWebApp').service('arrayOrderingSrv', function() {
var arrayOrderingSrv = this;
this.notebookListOrdering = function(note) {
return (note.name ? note.name : 'Note ' + note.id);
return arrayOrderingSrv.getNoteName(note);
};
this.getNoteName = function(note) {
if(note.name === undefined || note.name.trim() === '') {
return'Note ' + note.id;
} else {
return note.name;
}
};
});

View file

@ -33,6 +33,7 @@ angular.module('zeppelinWebApp').controller('LoginCtrl',
$rootScope.ticket = response.data.body;
angular.element('#loginModal').modal('toggle');
$rootScope.$broadcast('loginSuccess', true);
$rootScope.userName = $scope.loginParams.userName;
}, function errorCallback(errorResponse) {
$scope.loginParams.errorText = 'The username and password that you entered don\'t match.';
});

View file

@ -14,8 +14,8 @@
'use strict';
angular.module('zeppelinWebApp').controller('NavCtrl', function($scope, $rootScope, $routeParams,
$location, notebookListDataFactory, websocketMsgSrv, arrayOrderingSrv) {
angular.module('zeppelinWebApp').controller('NavCtrl', function($scope, $rootScope, $http, $routeParams,
$location, notebookListDataFactory, baseUrlSrv, websocketMsgSrv, arrayOrderingSrv) {
/** Current list of notes (ids) */
$scope.showLoginWindow = function() {
@ -71,6 +71,26 @@ angular.module('zeppelinWebApp').controller('NavCtrl', function($scope, $rootSco
loadNotes();
});
$scope.logout = function() {
$http.post(baseUrlSrv.getRestApiBase()+'/login/logout')
.success(function(data, status, headers, config) {
$rootScope.userName = '';
$rootScope.ticket.principal = '';
$rootScope.ticket.ticket = '';
$rootScope.ticket.roles = '';
BootstrapDialog.show({
message: 'Logout Success'
});
setTimeout(function() {
window.location = '#';
window.location.reload();
}, 1000);
}).
error(function(data, status, headers, config) {
console.log('Error %o %o', status, data.message);
});
};
$scope.search = function() {
$location.url(/search/ + $scope.searchTerm);
};

View file

@ -2,15 +2,25 @@
Licensed 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.
-->
-->
<script type="text/ng-template" id="notebook_list_renderer.html">
<a ng-if="note.id" href="#/notebook/{{note.id}}">{{noteName(note)}} </a>
<li ng-if="!note.id"
class="dropdown-submenu">
<a tabindex="-1" href="javascript: void(0)">{{noteName(note)}}</a>
<ul class="dropdown-menu">
<li ng-repeat="note in note.children track by $index" ng-class="{'active' : navbar.isActive(note.id)}" ng-include="'notebook_list_renderer.html'">
</li>
</ul>
</li>
</script>
<div class="navbar navbar-inverse navbar-fixed-top" style="display: none;" role="navigation" ng-class="{'displayNavBar': !asIframe}">
<div class="container">
<div class="navbar-header">
@ -25,7 +35,7 @@ limitations under the License.
</div>
<div class="collapse navbar-collapse" ng-controller="NavCtrl as navbar">
<ul class="nav navbar-nav">
<ul class="nav navbar-nav" ng-if="ticket">
<li class="dropdown" dropdown>
<a href="#" class="dropdown-toggle" dropdown-toggle>Notebook <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
@ -35,7 +45,8 @@ limitations under the License.
<li class="filter-names" ng-include="'components/filterNoteNames/filter-note-names.html'"></li>
<li ng-repeat="note in navbar.notes.list | filter:query | orderBy:navbar.arrayOrderingSrv.notebookListOrdering track by $index"
ng-class="{'active' : navbar.isActive(note.id)}">
<a href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}} </a>
<a href="#/notebook/{{note.id}}">{{noteName(note)}} </a>
<li ng-repeat="note in navbar.notes.root.children track by $index" ng-class="{'active' : navbar.isActive(note.id)}" ng-include="'notebook_list_renderer.html'">
</li>
</div>
</ul>
@ -50,7 +61,7 @@ limitations under the License.
<ul class="nav navbar-nav navbar-right" style="margin-top:10px; margin-right:5px;">
<li>
<li ng-if="ticket">
<!--TODO(bzz): move to Typeahead https://angular-ui.github.io/bootstrap -->
<form role="search"
style="width: 300px; display: inline-block; margin: 0 10px"
@ -83,6 +94,9 @@ limitations under the License.
<li ng-if="!ticket">
<button class="btn btn-default" data-toggle="modal" data-target="#loginModal" ng-click="showLoginWindow()" style="margin-left: 10px">Login</button>
</li>
<li ng-show="ticket.principal && ticket.principal!='anonymous'" style="left: 5px;">
<button class="btn btn-default" ng-click="logout()" tooltip-placement="bottom" tooltip="logout">Logout</button>
</li>
</ul>
</div>
</div>

View file

@ -28,6 +28,7 @@ limitations under the License.
placeholder="Note name" type="text" class="form-control"
id="noteName" ng-model="note.notename" ng-enter="notenamectrl.handleNameEnter()">
</div>
Use '/' to create folders. Example: /NoteDirA/Notebook1
</div>
<div class="modal-footer">
<button type="button" id="createNoteButton"

View file

@ -14,13 +14,52 @@
'use strict';
angular.module('zeppelinWebApp').factory('notebookListDataFactory', function() {
var notes = {};
notes.list = [];
var notes = {
root: {children: []},
flatList: [],
notes.setNotes = function(notesList) {
notes.list = angular.copy(notesList);
setNotes: function(notesList) {
// a flat list to boost searching
notes.flatList = angular.copy(notesList);
// construct the folder-based tree
notes.root = {children: []};
_.reduce(notesList, function(root, note) {
var noteName = note.name || note.id;
var nodes = noteName.match(/([^\/][^\/]*)/g);
// recursively add nodes
addNode(root, nodes, note.id);
return root;
}, notes.root);
}
};
var addNode = function(curDir, nodes, noteId) {
if (nodes.length === 1) { // the leaf
curDir.children.push({
name : nodes[0],
id : noteId
});
} else { // a folder node
var node = nodes.shift();
var dir = _.find(curDir.children,
function(c) {return c.name === node && c.children !== undefined;});
if (dir !== undefined) { // found an existing dir
addNode(dir, nodes, noteId);
} else {
var newDir = {
name : node,
hidden : true,
children : []
};
curDir.children.push(newDir);
addNode(newDir, nodes, noteId);
}
}
};
return notes;
});
});

View file

@ -60,10 +60,24 @@ angular.module('zeppelinWebApp').factory('websocketEvents', function($rootScope,
} else if (op === 'NOTES_INFO') {
$rootScope.$broadcast('setNoteMenu', data.notes);
} else if (op === 'AUTH_INFO') {
BootstrapDialog.alert({
closable: true,
title: 'Insufficient privileges',
message: data.info.toString()
BootstrapDialog.show({
closable: true,
title: 'Insufficient privileges',
message: data.info.toString(),
buttons: [{
label: 'Login',
action: function(dialog) {
dialog.close();
angular.element('#loginModal').modal({
show: 'true'
});
}
}, {
label: 'Cancel',
action: function(dialog){
dialog.close();
}
}]
});
} else if (op === 'PARAGRAPH') {
$rootScope.$broadcast('updateParagraph', data);

View file

@ -44,8 +44,8 @@ limitations under the License.
<link rel="stylesheet" href="bower_components/highlightjs/styles/github.css" />
<link rel="stylesheet" href="bower_components/ngtoast/dist/ngToast.css" />
<link rel="stylesheet" href="bower_components/bootstrap3-dialog/dist/css/bootstrap-dialog.min.css" />
<link rel="stylesheet" href="bower_components/datatables.net-bs/css/dataTables.bootstrap.css" />
<link rel="stylesheet" href="bower_components/datatables.net-buttons-bs/css/buttons.bootstrap.css" />
<link rel="stylesheet" href="bower_components/pikaday/css/pikaday.css" />
<link rel="stylesheet" href="bower_components/handsontable/dist/handsontable.css" />
<!-- endbower -->
<link rel="stylesheet" href="bower_components/jquery-ui/themes/base/all.css" />
<!-- endbuild -->
@ -132,16 +132,10 @@ limitations under the License.
<script src="bower_components/ngtoast/dist/ngToast.js"></script>
<script src="bower_components/ng-focus-if/focusIf.js"></script>
<script src="bower_components/bootstrap3-dialog/dist/js/bootstrap-dialog.min.js"></script>
<script src="bower_components/floatThead/dist/jquery.floatThead.js"></script>
<script src="bower_components/floatThead/dist/jquery.floatThead.min.js"></script>
<script src="bower_components/datatables.net/js/jquery.dataTables.js"></script>
<script src="bower_components/datatables.net-bs/js/dataTables.bootstrap.js"></script>
<script src="bower_components/datatables.net-buttons/js/dataTables.buttons.js"></script>
<script src="bower_components/datatables.net-buttons/js/buttons.colVis.js"></script>
<script src="bower_components/datatables.net-buttons/js/buttons.flash.js"></script>
<script src="bower_components/datatables.net-buttons/js/buttons.html5.js"></script>
<script src="bower_components/datatables.net-buttons/js/buttons.print.js"></script>
<script src="bower_components/datatables.net-buttons-bs/js/buttons.bootstrap.js"></script>
<script src="bower_components/zeroclipboard/dist/ZeroClipboard.js"></script>
<script src="bower_components/moment/moment.js"></script>
<script src="bower_components/pikaday/pikaday.js"></script>
<script src="bower_components/handsontable/dist/handsontable.js"></script>
<!-- endbower -->
<!-- endbuild -->
<!-- build:js({.tmp,src}) scripts/scripts.js -->

View file

@ -59,8 +59,10 @@ module.exports = function(config) {
'bower_components/ngtoast/dist/ngToast.js',
'bower_components/ng-focus-if/focusIf.js',
'bower_components/bootstrap3-dialog/dist/js/bootstrap-dialog.min.js',
'bower_components/floatThead/dist/jquery.floatThead.js',
'bower_components/floatThead/dist/jquery.floatThead.min.js',
'bower_components/zeroclipboard/dist/ZeroClipboard.js',
'bower_components/moment/moment.js',
'bower_components/pikaday/pikaday.js',
'bower_components/handsontable/dist/handsontable.js',
'bower_components/angular-mocks/angular-mocks.js',
// endbower
'src/app/app.js',

View file

@ -0,0 +1,79 @@
'use strict';
describe('Factory: NotebookList', function() {
var notebookList;
beforeEach(function () {
module('zeppelinWebApp');
inject(function ($injector) {
notebookList = $injector.get('notebookListDataFactory');
});
});
it('should generate both flat list and folder-based list properly', function() {
var notesList = [
{name: 'A', id: '000001'},
{name: 'B', id: '000002'},
{id: '000003'}, // notebook without name
{name: '/C/CA', id: '000004'},
{name: '/C/CB', id: '000005'},
{name: '/C/CB/CBA', id: '000006'}, // same name with a dir
{name: '/C/CB/CBA', id: '000007'}, // same name with another note
{name: 'C///CB//CBB', id: '000008'},
{name: 'D/D[A/DA]B', id:'000009'} // check if '[' and ']' considered as folder seperator
];
notebookList.setNotes(notesList);
var flatList = notebookList.flatList;
expect(flatList.length).toBe(9);
expect(flatList[0].name).toBe('A');
expect(flatList[0].id).toBe('000001');
expect(flatList[1].name).toBe('B');
expect(flatList[2].name).toBeUndefined();
expect(flatList[3].name).toBe('/C/CA');
expect(flatList[4].name).toBe('/C/CB');
expect(flatList[5].name).toBe('/C/CB/CBA');
expect(flatList[6].name).toBe('/C/CB/CBA');
expect(flatList[7].name).toBe('C///CB//CBB');
expect(flatList[8].name).toBe('D/D[A/DA]B');
var folderList = notebookList.root.children;
expect(folderList.length).toBe(5);
expect(folderList[0].name).toBe('A');
expect(folderList[0].id).toBe('000001');
expect(folderList[1].name).toBe('B');
expect(folderList[2].name).toBe('000003');
expect(folderList[3].name).toBe('C');
expect(folderList[3].id).toBeUndefined();
expect(folderList[3].children.length).toBe(3);
expect(folderList[3].children[0].name).toBe('CA');
expect(folderList[3].children[0].id).toBe('000004');
expect(folderList[3].children[0].children).toBeUndefined();
expect(folderList[3].children[1].name).toBe('CB');
expect(folderList[3].children[1].id).toBe('000005');
expect(folderList[3].children[1].children).toBeUndefined();
expect(folderList[3].children[2].name).toBe('CB');
expect(folderList[3].children[2].id).toBeUndefined();
expect(folderList[3].children[2].children.length).toBe(3);
expect(folderList[3].children[2].children[0].name).toBe('CBA');
expect(folderList[3].children[2].children[0].id).toBe('000006');
expect(folderList[3].children[2].children[0].children).toBeUndefined();
expect(folderList[3].children[2].children[1].name).toBe('CBA');
expect(folderList[3].children[2].children[1].id).toBe('000007');
expect(folderList[3].children[2].children[1].children).toBeUndefined();
expect(folderList[3].children[2].children[2].name).toBe('CBB');
expect(folderList[3].children[2].children[2].id).toBe('000008');
expect(folderList[3].children[2].children[2].children).toBeUndefined();
expect(folderList[4].name).toBe('D');
expect(folderList[4].id).toBeUndefined();
expect(folderList[4].children.length).toBe(1);
expect(folderList[4].children[0].name).toBe('D[A');
expect(folderList[4].children[0].id).toBeUndefined();
expect(folderList[4].children[0].children[0].name).toBe('DA]B');
expect(folderList[4].children[0].children[0].id).toBe('000009');
expect(folderList[4].children[0].children[0].children).toBeUndefined();
});
});

View file

@ -37,6 +37,7 @@ import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
@ -69,7 +70,8 @@ import com.google.common.collect.Lists;
public class LuceneSearch implements SearchService {
private static final Logger LOG = LoggerFactory.getLogger(LuceneSearch.class);
private static final String SEARCH_FIELD = "contents";
private static final String SEARCH_FIELD_TEXT = "contents";
private static final String SEARCH_FIELD_TITLE = "header";
static final String PARAGRAPH = "paragraph";
static final String ID_FIELD = "id";
@ -85,7 +87,7 @@ public class LuceneSearch implements SearchService {
try {
writer = new IndexWriter(ramDirectory, iwc);
} catch (IOException e) {
LOG.error("Failed to reate new IndexWriter", e);
LOG.error("Failed to create new IndexWriter", e);
}
}
@ -102,10 +104,12 @@ public class LuceneSearch implements SearchService {
try (IndexReader indexReader = DirectoryReader.open(ramDirectory)) {
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
Analyzer analyzer = new StandardAnalyzer();
QueryParser parser = new QueryParser(SEARCH_FIELD, analyzer);
MultiFieldQueryParser parser = new MultiFieldQueryParser(
new String[] {SEARCH_FIELD_TEXT, SEARCH_FIELD_TITLE},
analyzer);
Query query = parser.parse(queryStr);
LOG.debug("Searching for: " + query.toString(SEARCH_FIELD));
LOG.debug("Searching for: " + query.toString(SEARCH_FIELD_TEXT));
SimpleHTMLFormatter htmlFormatter = new SimpleHTMLFormatter();
Highlighter highlighter = new Highlighter(htmlFormatter, new QueryScorer(query));
@ -139,20 +143,33 @@ public class LuceneSearch implements SearchService {
LOG.debug(" Title: {}", doc.get("title"));
}
String text = doc.get(SEARCH_FIELD);
TokenStream tokenStream = TokenSources.getTokenStream(searcher.getIndexReader(), id,
SEARCH_FIELD, analyzer);
TextFragment[] frag = highlighter.getBestTextFragments(tokenStream, text, true, 3);
LOG.debug(" {} fragments found for query '{}'", frag.length, query);
for (int j = 0; j < frag.length; j++) {
if ((frag[j] != null) && (frag[j].getScore() > 0)) {
LOG.debug(" Fragment: {}", frag[j].toString());
}
}
String fragment = (frag != null && frag.length > 0) ? frag[0].toString() : "";
String text = doc.get(SEARCH_FIELD_TEXT);
String header = doc.get(SEARCH_FIELD_TITLE);
String fragment = "";
if (text != null) {
TokenStream tokenStream = TokenSources.getTokenStream(searcher.getIndexReader(), id,
SEARCH_FIELD_TEXT, analyzer);
TextFragment[] frag = highlighter.getBestTextFragments(tokenStream, text, true, 3);
LOG.debug(" {} fragments found for query '{}'", frag.length, query);
for (int j = 0; j < frag.length; j++) {
if ((frag[j] != null) && (frag[j].getScore() > 0)) {
LOG.debug(" Fragment: {}", frag[j].toString());
}
}
fragment = (frag != null && frag.length > 0) ? frag[0].toString() : "";
}
if (header != null) {
TokenStream tokenTitle = TokenSources.getTokenStream(searcher.getIndexReader(), id,
SEARCH_FIELD_TITLE, analyzer);
TextFragment[] frgTitle = highlighter.getBestTextFragments(tokenTitle, header, true, 3);
header = (frgTitle != null && frgTitle.length > 0) ? frgTitle[0].toString() : "";
} else {
header = "";
}
matchingParagraphs.add(ImmutableMap.of("id", path, // <noteId>/paragraph/<paragraphId>
"name", title, "snippet", fragment, "text", text));
"name", title, "snippet", fragment, "text", text, "header", header));
} else {
LOG.info("{}. No {} for this document", i + 1, ID_FIELD);
}
@ -252,11 +269,14 @@ public class LuceneSearch implements SearchService {
doc.add(new StringField("title", noteName, Field.Store.YES));
if (null != p) {
doc.add(new TextField(SEARCH_FIELD, p.getText(), Field.Store.YES));
doc.add(new TextField(SEARCH_FIELD_TEXT, p.getText(), Field.Store.YES));
if (p.getTitle() != null) {
doc.add(new TextField(SEARCH_FIELD_TITLE, p.getTitle(), Field.Store.YES));
}
Date date = p.getDateStarted() != null ? p.getDateStarted() : p.getDateCreated();
doc.add(new LongField("modified", date.getTime(), Field.Store.NO));
} else {
doc.add(new TextField(SEARCH_FIELD, noteName, Field.Store.YES));
doc.add(new TextField(SEARCH_FIELD_TEXT, noteName, Field.Store.YES));
}
return doc;
}

View file

@ -65,8 +65,8 @@ public class LuceneSearchTest {
@Test public void canIndexNotebook() {
//give
Note note1 = newNoteWithParapgraph("Notebook1", "test");
Note note2 = newNoteWithParapgraph("Notebook2", "not test");
Note note1 = newNoteWithParagraph("Notebook1", "test");
Note note2 = newNoteWithParagraph("Notebook2", "not test");
List<Note> notebook = Arrays.asList(note1, note2);
//when
@ -75,8 +75,8 @@ public class LuceneSearchTest {
@Test public void canIndexAndQuery() {
//given
Note note1 = newNoteWithParapgraph("Notebook1", "test");
Note note2 = newNoteWithParapgraphs("Notebook2", "not test", "not test at all");
Note note1 = newNoteWithParagraph("Notebook1", "test");
Note note2 = newNoteWithParagraphs("Notebook2", "not test", "not test at all");
notebookIndex.addIndexDocs(Arrays.asList(note1, note2));
//when
@ -91,8 +91,8 @@ public class LuceneSearchTest {
@Test public void canIndexAndQueryByNotebookName() {
//given
Note note1 = newNoteWithParapgraph("Notebook1", "test");
Note note2 = newNoteWithParapgraphs("Notebook2", "not test", "not test at all");
Note note1 = newNoteWithParagraph("Notebook1", "test");
Note note2 = newNoteWithParagraphs("Notebook2", "not test", "not test at all");
notebookIndex.addIndexDocs(Arrays.asList(note1, note2));
//when
@ -104,9 +104,31 @@ public class LuceneSearchTest {
assertThat(results.get(0)).containsEntry("id", note1.getId());
}
@Test
public void canIndexAndQueryByParagraphTitle() {
//given
Note note1 = newNoteWithParagraph("Notebook1", "test", "testingTitleSearch");
Note note2 = newNoteWithParagraph("Notebook2", "not test", "notTestingTitleSearch");
notebookIndex.addIndexDocs(Arrays.asList(note1, note2));
//when
List<Map<String, String>> results = notebookIndex.query("testingTitleSearch");
//then
assertThat(results).isNotEmpty();
assertThat(results.size()).isAtLeast(1);
int TitleHits = 0;
for (Map<String, String> res : results) {
if (res.get("header").contains("testingTitleSearch")) {
TitleHits++;
}
}
assertThat(TitleHits).isAtLeast(1);
}
@Test public void indexKeyContract() throws IOException {
//give
Note note1 = newNoteWithParapgraph("Notebook1", "test");
Note note1 = newNoteWithParagraph("Notebook1", "test");
//when
notebookIndex.addIndexDoc(note1);
//then
@ -129,8 +151,8 @@ public class LuceneSearchTest {
@Test public void canIndexAndReIndex() throws IOException {
//given
Note note1 = newNoteWithParapgraph("Notebook1", "test");
Note note2 = newNoteWithParapgraphs("Notebook2", "not test", "not test at all");
Note note1 = newNoteWithParagraph("Notebook1", "test");
Note note2 = newNoteWithParagraphs("Notebook2", "not test", "not test at all");
notebookIndex.addIndexDocs(Arrays.asList(note1, note2));
//when
@ -155,8 +177,8 @@ public class LuceneSearchTest {
@Test public void canDeleteFromIndex() throws IOException {
//given
Note note1 = newNoteWithParapgraph("Notebook1", "test");
Note note2 = newNoteWithParapgraphs("Notebook2", "not test", "not test at all");
Note note1 = newNoteWithParagraph("Notebook1", "test");
Note note2 = newNoteWithParagraphs("Notebook2", "not test", "not test at all");
notebookIndex.addIndexDocs(Arrays.asList(note1, note2));
assertThat(resultForQuery("Notebook2")).isNotEmpty();
@ -174,8 +196,8 @@ public class LuceneSearchTest {
@Test public void indexParagraphUpdatedOnNoteSave() throws IOException {
//given: total 2 notebooks, 3 paragraphs
Note note1 = newNoteWithParapgraph("Notebook1", "test");
Note note2 = newNoteWithParapgraphs("Notebook2", "not test", "not test at all");
Note note1 = newNoteWithParagraph("Notebook1", "test");
Note note2 = newNoteWithParagraphs("Notebook2", "not test", "not test at all");
notebookIndex.addIndexDocs(Arrays.asList(note1, note2));
assertThat(resultForQuery("test").size()).isEqualTo(3);
@ -199,8 +221,8 @@ public class LuceneSearchTest {
@Test public void indexNoteNameUpdatedOnNoteSave() throws IOException {
//given: total 2 notebooks, 3 paragraphs
Note note1 = newNoteWithParapgraph("Notebook1", "test");
Note note2 = newNoteWithParapgraphs("Notebook2", "not test", "not test at all");
Note note1 = newNoteWithParagraph("Notebook1", "test");
Note note2 = newNoteWithParagraphs("Notebook2", "not test", "not test at all");
notebookIndex.addIndexDocs(Arrays.asList(note1, note2));
assertThat(resultForQuery("test").size()).isEqualTo(3);
@ -226,17 +248,23 @@ public class LuceneSearchTest {
* @param parText text of the paragraph
* @return Note
*/
private Note newNoteWithParapgraph(String noteName, String parText) {
private Note newNoteWithParagraph(String noteName, String parText) {
Note note1 = newNote(noteName);
addParagraphWithText(note1, parText);
return note1;
}
private Note newNoteWithParagraph(String noteName, String parText,String title) {
Note note = newNote(noteName);
addParagraphWithTextAndTitle(note, parText, title);
return note;
}
/**
* Creates a new Note \w given name,
* adds N paragraphs \w given texts
*/
private Note newNoteWithParapgraphs(String noteName, String... parTexts) {
private Note newNoteWithParagraphs(String noteName, String... parTexts) {
Note note1 = newNote(noteName);
for (String parText : parTexts) {
addParagraphWithText(note1, parText);
@ -250,6 +278,13 @@ public class LuceneSearchTest {
return p;
}
private Paragraph addParagraphWithTextAndTitle(Note note, String text, String title) {
Paragraph p = note.addParagraph();
p.setText(text);
p.setTitle(title);
return p;
}
private Note newNote(String name) {
Note note = new Note(notebookRepoMock, replLoaderMock, null, notebookIndex, null);
note.setName(name);