Created on 08-25-2017 01:11 PM
This article is the first in a series of articles discussing various topics regarding frontend UI/UX development on the Apache NiFi project (versions 1.1.2+). This series will explore some of the architecture and bootstrapping of the client side JavaScript as well as various "recipes", case studies, and examples on how to accomplish certain tasks.
Keeping up with modern web development best practices, frameworks, and anti-patterns can be intimidating. Frameworks like AngularJS, Ember.js, Knockout, Backbone.js turned the JavaScript world inside out just 3-5 years ago with their declarative styles and TDD approach. Couple that with all the ever changing ECMAscript standard, differing support of said standard in various browsers, and the fact that as memory and storage capabilities have continued to increase and at lower price points more and more functionality is expected from, and placed on, the client and you start to understand that web development is still maturing and ever changing. The tooling around web development has also added to the insanity. Polyfills to add new features/capabilities to old browsers and transpilers that offer future ECMSscript6 functionality in today's "modern" browsers. Terminology like “module bundlers vs. module loaders,” “RequireJS vs. SystemJS”, and “AMD vs. CommonJS vs. UMD vs. NativeJS(ES5 vs. ES6)”. It can quickly become overwhelming but understanding these terms is vital for web developers who desire to contribute client side code in the Apache NiFi project.
In this article we will discuss a few of the topics mentioned above and then walk through a couple of case studies complete with in depth examples and appropriate exercises. Once you have read it you should feel comfortable with:
The Apache NiFi project strives to deliver a maintainable, testable, extensible, and reusable code base. For the client the next step in working towards these long term goals was to remove any global references to NiFi's already conceptually modular JavaScript components and then resolve any circular references. This does a couple of things
Previous to Apache NiFi version 1.1.2 the `nf` namespace was added to the browser's global `window` object as a container for all NiFi client modules. As each module was loaded they would add themselves to the `nf` object. Whenever a module wanted to access methods in another module:
//Previous to Apache NiFi version 1.1.2 the way a developer would access methods from one module in a different module was to gain access to it from the global scope `nf` object. nf.someModule.someFunction()
The issue with this approach is that there is nothing stopping developers from having two modules depend on each other and creating circular references. YUCK!
Starting with Apache NiFi 1.1.2, in order to prevent developers from inadvertently creating circular references each JavaScript module is now wrapped in an IIFE (Immediately Invoked Function Expression that runs as soon as it is defined). Once invoked, a Universal Module Definition (UMD) pattern is leveraged to detect, accept, and inject JavaScript modules loaded via ANY module loader (like RequireJS, SystemJS, WebPack, ..., or even script tags in the document head!). Finally, the module factory returns and the module is added to the global `nf` for storage and retrieval. The reason the UMD pattern was leveraged was because it supports all of the existing JavaScript module loader/bundlers and as of this writing the nifi-web-ui client is not utilizing any of them (RequireJS, SystemJs, WebPack, ...). Eventually a module loader may be introduced (and subsequently a module bundler to replace our custom bundler...but that is a separate effort another post!) but until then modules are still being loaded via the script tags in the document head with one major difference. The order is strictly enforced.
Let's consider the following example of a JavaScript module that has jquery injected into its scope:
// Note: `root` is the browser global `window` object. (function (root, factory) { if (typeof define === 'function' && define.amd) { //If an AMD module loader is requesting this module. //NOTE: The NiFi canvas application does not use an AMD module loader(see[5]). define(['jquery'], function ($) { return (root.nf.ModuleFactory = factory($)); }); } else if (typeof exports === 'object' && typeof module === 'object') { //If CommonJs module loader is requesting this module. //NOTE: The NiFi canvas application does not use an AMD module loader(see[5]). module.exports = (root.nf.ModuleFactory = factory(require('jquery'))); } else { //If no module loader is being used then inject jquery from the global `$` namespace. //NOTE: This is the way that the NiFi canvas application loads JS modules root.nf.ModuleFactory = factory(root.$); } }(this, function ($) { //Use the injected jquery '$' var body = $('body'); // private method function notHelloOrGoodbye(){ //Use the injected jquery var body = $('body'); }; // public methods (see below) function hi(){}; function bye(){}; // Exposed public methods return { hello: hi, goodbye: bye }; }));
Now, the experienced JavaScript developer may notice:
Each module needs the module(s) it depends on to be injected into its scope but NiFi JavaScript modules are reusable and need to be agnostic to the module loading strategy being used. Module loaders like RequireJS and SystemJS provide a type of cache which allows developers to store and retrieve JavaScript modules and UMD supports that use case. What about the case when a developer simply wants to load a NiFi JavaScript module with a script tag? It turns out that our `nf` namespace (remember the `nf` object?) is already acting as a type of cache and so by leveraging the UMD pattern and existing browser capabilities we are able to 'roll our own' JavaScript module loader in the case that our NiFi modules are being NOT being loaded with a module loader.
The nf-canvas-bootstrap.js module is the last module loaded but the first piece of code that runs when the NiFi canvas starts up. It waits for the DOM ready event to be fired, then it configures and bootstraps the angular application (more on that in a follow up article), and then is responsible for initializing the rest of the modules (many of which are dynamically manipulating the DOM) . During this startup phase control of a few Javascript modules are inverted. As a matter of convenience the injected modules are listed below:
Module | Injected with a reference to: |
---|---|
nf-canvas-utils.js |
|
nf-quickselect.js |
|
nf-shell.js |
|
nf-component-version.js |
|
nf-connection-configuration.js |
|
nf-controller-service.js |
|
nf-reporting-task.js |
|
nf-context-menu.js |
|
[Table 1]
There are four main strategical advantages gained by leveraging the UMD pattern, decoupling circular references, and leveraging inversion of control during the NiFi bootstrapping process:
In this section we will explore the nf-context-menu.js [2] module and how the "right-click" event listener is bound to each component displayed on the NiFi canvas. Below are links to each of the NiFi canvas component types available:
Each of these NiFi canvas component type modules are responsible for several things:
However, for this case study there are a couple responsibilities that deserve a special mention:
Example: NiFi canvas 'Label' component:
The nf-graph.js module inverts control of the nf-context-menu.js module to the nf-label.js module during its initialization (which, consequently, is the last module initialized in the bootstrapping process). When the nf-label.js module renders a 'Label' component it leverages the D3 3.x library to attach a listener to that particular component for the 'contextmenu.selection' event.
Exercise 1:
One of the Apache NiFi community members recently submitted a PR to add a 'quick select' capability to components on the canvas that listens for a 'dblclick' event on each component and then opens the configuration dialog for that component. This is a great example of how to implement event listeners and I encourage you to review the PR here and then attempt to add your own.
At this point we have a good understanding of how the NiFi canvas context menu is requested for a single component on the canvas but what about when multiple components on the canvas are selected? Let's examine how a requested NiFi canvas context menu determines what menu items are available and how the nf-context-menu.js module is able to invoke actions in the application:
Example: How NiFi context menu items are determined based on components selected on the canvas:
No matter how many components on the canvas are selected the context menu is actually requested by the component on the canvas that is right clicked. However, the list of actions available in the requested context menu are based on the selected components. So, how do we get the collection of selected components? Well, good news, the nf-canvas-utils.js module is capable of returning a list of the currently selected components on the canvas. Ok, so now that we know how to get the selection where is it determined which menu items to include in the context menu. The way this process works is pretty straight forward. Each menu item is an object in a list. Most of the properties of a menu item object are for things like the display text and the displayed icon for a context menu item. However, there are two properties that deserve some special attention:
Exercise 2:
Add a new menu item to the context menu. (Hint: you will need to reuse an existing private condition function from the nf-context-meu.js module or you will need to add a new one. You will also need to reuse an existing publicly available nf-actions.js module function or you will need to add a new one.)
Whew! That was a lot to cover and a lot of the information here is pretty dense. I hope you found this informative and helpful in your quest to contribute to the client side code for Apache NiFi. I look forward to addressing any questions and thanks for reading!
NOTES:
[1] The nf-canvas.js should not be injected in any other module except for nf-canvas-utils.js and any direct interaction with the nf-canvas.js should be avoided. Use the nf-canvas-utils.js module to interface with the nf-canvas.js module.
[2] The nf-context-menu.js, as you may suspect, is responsible for rendering and maintaining the available menu options listed for the rendered NiFi canvas context menu.
[3] The nf-graph.js module depends on and acts as an interface to of each of the NiFi canvas component type modules throughout the NiFi canvas application.
[4] The D3 JavaScript Library is used extensively throughout the NiFi canvas application. This article does not focus on best practices nor does it offer any in depth explanation on how D3 is implemented or its expected behavior. A nice blog discussing D3 and context menus can be found here.
[5] If a developer is building a custom processor UI and wants to use RequireJs to load nf-common.js that functionality is there. If some other developer wants to use SystemJS to load nf-common.js that functionality is supported by the JavaScript modules provided by Apache NiFi.
Created on 01-01-2019 09:09 PM
Excellent topic.. many thanks
Created on 01-01-2019 09:10 PM
on side question.
I'm trying to create custom processor where Scheduling tab is disabled, or cant be modified.
is this possible ?