GitHub user doanduyhai opened a pull request: https://github.com/apache/incubator-zeppelin/pull/678
[Zeppelin-] AngularJS utilities functions and Hide/Show ParagraphId feature ### What is this PR for? This is a PR to add **utilities functions for the AngularJS system** and a new **Hide/Show Paragraph Id** feature. > See screenshots below to have a feeling of how it works #### 1) AngularJS Utility Functions * **runParagraph('paragraphId')** : run a paragraph given its id * **runParagraphs(['paragraphId1', 'paragraphId2'])** : run a list of paragraphs given their id * **pushToServer('varName', value, parameters)** : push a variable _varName_ with its _value_ to the server/interpreter **angular object repository** using current **noteId** and provided interpreter(s) (and optionally paragraph id(s)). _parameters_ is a map with the following properties: Ex: ```javascript parameters = { interpreter: 'spark', interpreters: ['md', 'sh'], paragraph: '20160126-153136_1060166247', paragraphs: ['20160126-171914_843040190', '20160126-181556_1915782845'], scope: 'note', runParagraphs: false } ``` * **interpreter**: name of the interpreter to be used for angular object repository lookup * **interpreters**: name of the interpreters to be used for angular object repository lookup * **paragraph**: paragraph id used to update the angular object repository * **paragraphs**: paragraph ids used to update the angular object repository * **scope**: scope of the angular object, can be _note_ or _paragraph_. Default = _paragraph_ * **runParagraphs**: if _true_, run the paragraphs listed by the properties **paragraph** or **paragraphs**. Default = _true_ <br/> Remarks: > 1. if both **interpreter** and **interpreters** are provided, the single **interpreter** will be added to the **interpreters** list. Same remark for **paragraph** and **paragraphs** > 2. if a list of **interpreters** and **paragraphs** are provided, **for each interpreter the object will be pushed only to the paragraphs which matches this interpreter** > 3. if one or several **paragraph** is provided, the object will be pushed to the registry using the _(current note id, paragraph id)_ scope. This correspond to the default _paragraph_ value for the **scope** variable. However, it is also possible to save the same object to the _current note id_ scope. In this case set the **scope** variable to _note_. In this case the object will be saved using paragraph scope and note scope > 4. if **scope** = _paragraph_ and no paragraph id is provided, the object will be saved using the _current note id_ scope > 5. if **runParagraphs** = _true_ and no paragraph id is provided, it will be ignored > 6. set **runParagraphs** to _false_ if you want to push the variable to the back-end without refreshing paragraph * **removeFromServer('varName', parameters)**: remove the variable _varName_ from server/interpreter **angular object repository** using current **noteId**. Ex: ```javascript parameters = { interpreter: 'spark', interpreters: ['md', 'sh'], paragraph: '20160126-153136_1060166247', paragraphs: ['20160126-171914_843040190', '20160126-181556_1915782845'] } ``` * **interpreter**: name of the interpreter to be used for angular object repository lookup * **interpreters**: name of the interpreters to be used for angular object repository lookup * **paragraph**: paragraph id used to remove object from the angular object repository * **paragraphs**: paragraph ids used to remove object from the angular object repository Remarks: > 1. if both **interpreter** and **interpreters** are provided, the single **interpreter** will be added to the **interpreters** list. Same remark for **paragraph** and **paragraphs** > 2. if a list of **interpreters** and **paragraphs** are provided, **for each interpreter the object will be pushed only to the paragraphs which matches this interpreter** > 3. if one or several **paragraph** is provided, the object will be remove from the registry using the _(current note id, paragraph id)_ scope > 4. if no **paragraph** is provided, the object will be remove from the registry using the _current note id_ scope #### 2) Hide/Show Paragraph Id This feature is necessary for the above AngularJS functions to work. One can always retrieve the current paragraph Id using the code ```scala z.getInterpreterContext().getParagraphId() ``` But it is far from being user-friendly. Therefore, now you can show or hide paragraph Id using a toggle. This is placed at the same place as Show/Hide line numbers. The paragraph id is displayed at the top left corner, above the Title. I'm completely open to any suggestion with regard to the location of the display #### 3) Technical Impl 1. First I have added a new **Thrift** method _angularRegistryPush()_. This method is used to push the current angular object registry from the Zeppelin server to the remote interpreter. According to @leemoonsoo, when a note is loaded from the JSON file, the interpreter **may not be initialized** yet so we need to store all the angular variables in the local registry. As soon as the remote interpreter is started, we can call the _angularRegistryPush()_ method to push the local registry to the remote interpreter 2. A new public constructor has been added to the `AngularObject` class. The reason is because when we push the whole `Map<String, Map<String, AngularObject>>` registry to the remote interpreter using _angularRegistryPush()_, the registry is serialized using **Gson**. At the remote interpreter side, the payload is deserialized back into a `Map<String, Map<String, AngularObject>>`. If a class **do not have a public constructor** then **Gson** will use some `Unsafe` tricks to create it (see **[here]**). Consequently, the `private transient List<AngularObjectWatcher> watchers = new LinkedList<AngularObjectWatcher>();` property will NOT be initialized and will throw `NullPointerException` later when the code access the `watchers` 3. The `runParagraph()` and `runParagraphs()` Angular functions are injected into the **isolated scope** create for each paragraph so that people can access them. Currently I'm using the following trick to retrieve the paragraph details for a given paragraphId: ```javascript var paragraphDiv = angular.element('#' + paragraphId + '_paragraphColumn_main[ng-controller="ParagraphCtrl"]'); var paragraph = paragraphDiv.scope().paragraph; ``` It's not the cleanest way. Ideally, we should inject into the isolated scope the `note.paragraphs` array from the Note scope but I'm not sure if it is recommended to leak such information into an isolated scope. Any idea/suggestion is welcomed 4. New message operations **`ANGULAR_OBJECT_CLIENT_UPDATE`** and **`ANGULAR_OBJECT_CLIENT_REMOVE`** have been added to support `pushToServer()` and `removeFromServer()` Angular functions. The core functionalities are implemented in `NotebookServer.angularObjectClientUpdate()` and `NotebookServer.angularObjectClientDelete()`. I also add 8 unit tests for those 2 methods. > A remark on the unit tests. I had to mock a bunch of classes for the tests, which suggest that the code is somehow entangled. I try to make the method as modular as possible but there are a lot of dependencies on objects like Notebook, Note, InterpreterGroup etc ... Maybe a general refactoring later would be nice 5. The `ZeppelinContext.getAngularObject()` private method has been updated. Right now it look for variable using only the note scope. I change the code so that now, we look for a variable using: * first _(noteId, current paragraphId)_ as key * if no object found, then use _noteId_ as key * if no object found, then look at the **global scope** 6. Last but not least, **I have changed the current behavior of dynamic form**. In the method `Paragraph.jobRun()`, when we encounter a variable definition block ( ${var = ...}) while parsing, we first look into the **angular object registry** using _(noteId, paragraphId)_ scope, then _(noteId)_ scope. If we find a matching variable we'll use it and **we do not create any form input for the paragraph**. At worst, we can just remove the last commit, it is independent from the other features. @Leemoonsoo @jongyoul @bzz @felixcheung @corneadoug I really need your remarks/comments for the above 6 technical points. Thanks ### What type of PR is it? [Feature] ### Todos * [ ] - Code review by committers/contributors/community * [ ] - Responses and suggestions for the technical questions raised in chapter **3) Technical Impl** * [ ] - Features tested following the below step ### Is there a relevant Jira issue? **[ZEPPELIN-635]** ### How should this be tested? **See below 12 screenshots for all possible test scenarios** ### Screenshots (if appropriate) **Hide/Show ParagraphId**  **Run Single Paragraph**  **Run Multiple Paragraphs**  **Push To Server Scenario 1**: single interpreter, **note** scope  **Push To Server Scenario 2**: multiple interpreters, **note** scope  **Push To Server Scenario 3**: single interpreter, single paragraph  **Push To Server Scenario 4**: single interpreter, multiples paragraphs  **Push To Server Scenario 5**: single interpreter, multiples paragraphs AND note scope  **Push To Server Scenario 6**: single interpreter, multiples paragraphs AND run paragraphs  **Push To Server Scenario 7**: multiple interpreters, multiples paragraphs, **overriding dynamic forms**  **Remove From Server Scenario 1**: single interpreter, single paragraph  **Remove From Server Scenario 2**: single interpreter, **note** scope  ### Questions: * Does the licenses files need update? --> **No** * Is there breaking changes for older versions? --> **No** * Does this needs documentation? --> **Yes** [here]: http://stackoverflow.com/questions/18645050/is-default-no-args-constructor-mandatory-for-gson [ZEPPELIN-635]: https://issues.apache.org/jira/browse/ZEPPELIN-635 You can merge this pull request into a Git repository by running: $ git pull https://github.com/doanduyhai/incubator-zeppelin PushToServer_Angular_Function Alternatively you can review and apply these changes as the patch at: https://github.com/apache/incubator-zeppelin/pull/678.patch To close this pull request, make a commit to your master/trunk branch with (at least) the following in the commit message: This closes #678 ---- commit 7c1e284b0509e9cd8419fe83485643d1a7ac7e66 Author: DuyHai DOAN <doanduy...@gmail.com> Date: 2016-01-24T18:55:30Z Add Thrift RPC method angularRegistryPush() commit ecb3a219478cc257d5069faccb00227c497569a0 Author: DuyHai DOAN <doanduy...@gmail.com> Date: 2016-01-26T15:50:10Z Make AngularObject constructor public because of serialization issue commit 65985cab69f337c1e31c7d667defd79b94b334ed Author: DuyHai DOAN <doanduy...@gmail.com> Date: 2016-01-26T19:30:36Z Implement runParagraph() and runParagraphs() Angular functions commit 6eeaa4bd02ca78df9259342f9917c4f62b024c37 Author: DuyHai DOAN <doanduy...@gmail.com> Date: 2016-01-26T12:41:12Z Implement pushToServer() Angular function commit 96c30798b80a43ff6518c39138cb7a19361f5843 Author: DuyHai DOAN <doanduy...@gmail.com> Date: 2016-01-26T15:51:11Z ZeppelinContext angular() method should look for variable using the paragraph scope, then note scope and then global scope, in this order commit 21a12ddd2d3ca2b81ef20e5fa3db6a3ab4793227 Author: DuyHai DOAN <doanduy...@gmail.com> Date: 2016-01-24T20:31:53Z Add Hide/Show paragraph id feature commit b544b0c47bd3af05e7a85a1d9d12b260e142988e Author: DuyHai DOAN <doanduy...@gmail.com> Date: 2016-01-25T20:13:53Z Replace dynamic form with angular object from registry if exists commit bf4b29317c6e0aa09e48a47cb8a0849016362fa8 Author: DuyHai DOAN <doanduy...@gmail.com> Date: 2016-01-26T19:29:20Z Implement removeFromServer() Angular function ---- --- If your project is set up for it, you can reply to this email and have your reply appear on GitHub as well. If your project does not have this feature enabled and wishes so, or if the feature is enabled but not working, please contact infrastructure at infrastruct...@apache.org or file a JIRA ticket with INFRA. ---