Merge branch 'master' into ZEPPELIN-732-up
|
|
@ -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
|
|
@ -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
|
||||
```
|
||||
|
||||
[](https://github.com/igrigorik/ga-beacon)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 155 KiB |
BIN
docs/assets/themes/zeppelin/img/screenshots/z_angularBind.gif
Normal file
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 105 KiB |
BIN
docs/assets/themes/zeppelin/img/screenshots/z_angularUnbind.gif
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
docs/assets/themes/zeppelin/img/screenshots/z_runParagraph.gif
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/configuration_menu.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/homepage.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/interpreter_menu.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/note_commands.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/note_configuration.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/note_import_dialog.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/note_paragraph_layout.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/note_toolbar.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/notebook_menu.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/notes_management.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/assets/themes/zeppelin/img/ui-img/paragraph_layout.png
Normal file
|
After Width: | Height: | Size: 589 KiB |
|
|
@ -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.
|
||||
159
docs/displaysystem/front-end-angular.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
139
docs/ui_layout/zeppelin_layout.md
Normal 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" />
|
||||
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
"nv": false,
|
||||
"ace": false,
|
||||
"d3": false,
|
||||
"BootstrapDialog": false
|
||||
"BootstrapDialog": false,
|
||||
"Handsontable": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}]
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
79
zeppelin-web/test/spec/factory/notebookList.js
Normal 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();
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||