You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ma...@apache.org on 2015/04/10 15:43:38 UTC

[03/62] [abbrv] incubator-nifi git commit: Squashed commit of the following:

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
index 3d573b3..0e60b2c 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
@@ -14,33 +14,45 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf, Slick */
+
 nf.Settings = (function () {
 
     var config = {
+        node: 'node',
+        ncm: 'ncm',
+        filterText: 'Filter',
+        styles: {
+            filterList: 'filter-list'
+        },
         urls: {
             controllerConfig: '../nifi-api/controller/config',
-            controllerArchive: '../nifi-api/controller/archive'
+            controllerArchive: '../nifi-api/controller/archive',
+            controllerServiceTypes: '../nifi-api/controller/controller-service-types',
+            controllerServices: '../nifi-api/controller/controller-services',
+            reportingTaskTypes: '../nifi-api/controller/reporting-task-types',
+            reportingTasks: '../nifi-api/controller/reporting-tasks'
         }
     };
 
-    /**
-     * Marshals the details to include in the configuration request.
-     */
-    var marshalConfiguration = function () {
-        // create the configuration
-        var configuration = {};
-        configuration['name'] = $('#data-flow-title-field').val();
-        configuration['comments'] = $('#data-flow-comments-field').val();
-        configuration['maxTimerDrivenThreadCount'] = $('#maximum-timer-driven-thread-count-field').val();
-        configuration['maxEventDrivenThreadCount'] = $('#maximum-event-driven-thread-count-field').val();
-        return configuration;
+    var gridOptions = {
+        forceFitColumns: true,
+        enableTextSelectionOnCells: true,
+        enableCellNavigation: true,
+        enableColumnReorder: false,
+        autoEdit: false,
+        multiSelect: false
     };
 
-    return {
-        /**
-         * Initializes the status page.
-         */
-        init: function () {
+    /**
+     * Initializes the general tab.
+     */
+    var initGeneral = function () {
+        // update the visibility of the controls
+        if (nf.Common.isDFM()) {
+            $('#general-settings div.editable').show();
+            $('#general-settings div.read-only').hide();
 
             // register the click listener for the archive link
             $('#archive-flow-link').click(function () {
@@ -63,13 +75,7 @@ nf.Settings = (function () {
                         dialogContent: 'A new flow archive was successfully created.',
                         overlayBackground: false
                     });
-                }).fail(function (xhr, status, error) {
-                    // close the details panel
-                    $('#settings-cancel').click();
-
-                    // handle the error
-                    nf.Common.handleAjaxError(xhr, status, error);
-                });
+                }).fail(nf.Common.handleAjaxError);
             });
 
             // register the click listener for the save button
@@ -98,27 +104,1629 @@ nf.Settings = (function () {
                     $('#data-flow-title-container').children('span.link:first-child').text(response.config.name);
 
                     // close the settings dialog
+                    nf.Dialog.showOkDialog({
+                        dialogContent: 'Settings successfully applied.',
+                        overlayBackground: false
+                    });
+                }).fail(nf.Common.handleAjaxError);
+            });
+        } else {
+            $('#general-settings div.editable').hide();
+            $('#general-settings div.read-only').show();
+        }
+    };
+
+    /**
+     * Marshals the details to include in the configuration request.
+     */
+    var marshalConfiguration = function () {
+        // create the configuration
+        var configuration = {};
+        configuration['name'] = $('#data-flow-title-field').val();
+        configuration['comments'] = $('#data-flow-comments-field').val();
+        configuration['maxTimerDrivenThreadCount'] = $('#maximum-timer-driven-thread-count-field').val();
+        configuration['maxEventDrivenThreadCount'] = $('#maximum-event-driven-thread-count-field').val();
+        return configuration;
+    };
+
+    /**
+     * Get the text out of the filter field. If the filter field doesn't
+     * have any text it will contain the text 'filter list' so this method
+     * accounts for that.
+     */
+    var getControllerServiceTypeFilterText = function () {
+        var filterText = '';
+        var filterField = $('#controller-service-type-filter');
+        if (!filterField.hasClass(config.styles.filterList)) {
+            filterText = filterField.val();
+        }
+        return filterText;
+    };
+
+    /**
+     * Filters the processor type table.
+     */
+    var applyControllerServiceTypeFilter = function () {
+        // get the dataview
+        var controllerServiceTypesGrid = $('#controller-service-types-table').data('gridInstance');
+
+        // ensure the grid has been initialized
+        if (nf.Common.isDefinedAndNotNull(controllerServiceTypesGrid)) {
+            var controllerServiceTypesData = controllerServiceTypesGrid.getData();
+
+            // update the search criteria
+            controllerServiceTypesData.setFilterArgs({
+                searchString: getControllerServiceTypeFilterText(),
+                property: $('#controller-service-type-filter-options').combo('getSelectedOption').value
+            });
+
+            // need to invalidate the entire table since parent elements may need to be 
+            // rerendered due to changes in their children
+            controllerServiceTypesData.refresh();
+            controllerServiceTypesGrid.invalidate();
+        }
+    };
+
+    /**
+     * Determines if all of the ancestors of the specified item are expanded.
+     * 
+     * @param {type} item
+     * @returns {Boolean}
+     */
+    var areAncestorsExpanded = function (item) {
+        var documentedType = item;
+        while (documentedType.parent !== null) {
+            if (documentedType.parent.collapsed === true) {
+                return false;
+            }
+            documentedType = documentedType.parent;
+        }
+
+        return true;
+    };
+
+    /**
+     * Determines if the specified item is an ancestor.
+     * 
+     * @param {type} item
+     * @returns {Boolean}
+     */
+    var isAncestor = function (item) {
+        return item.children.length > 0;
+    };
+
+    /**
+     * Hides the selected controller service.
+     */
+    var clearSelectedControllerService = function () {
+        if (nf.Canvas.isClustered()) {
+            $('#controller-service-availability-combo').combo('setSelectedOption', {
+                value: config.node
+            });
+        }
+        
+        $('#controller-service-type-description').text('');
+        $('#controller-service-type-name').text('');
+        $('#selected-controller-service-name').text('');
+        $('#selected-controller-service-type').text('');
+        $('#controller-service-description-container').hide();
+    };
+
+    /**
+     * Clears the selected controller service type.
+     */
+    var clearControllerServiceSelection = function () {
+        // clear the selected row
+        clearSelectedControllerService();
+
+        // clear the active cell the it can be reselected when its included
+        var controllerServiceTypesGrid = $('#controller-service-types-table').data('gridInstance');
+        controllerServiceTypesGrid.resetActiveCell();
+    };
+
+    /**
+     * Performs the filtering.
+     * 
+     * @param {object} item     The item subject to filtering
+     * @param {object} args     Filter arguments
+     * @returns {Boolean}       Whether or not to include the item
+     */
+    var filterControllerServiceTypes = function (item, args) {
+        if (!areAncestorsExpanded(item)) {
+            // if this item is currently selected and its parent is not collapsed
+            if ($('#selected-controller-service-type').text() === item['type']) {
+                clearControllerServiceSelection();
+            }
+
+            // update visibility flag
+            item.visible = false;
+
+            return false;
+        }
+
+        // don't allow ancestors to be filtered out (unless any of their ancestors
+        // are collapsed)
+        if (isAncestor(item)) {
+            // update visibility flag
+            item.visible = false;
+
+            return true;
+        }
+
+        // determine if the item matches the filter
+        var matchesFilter = matchesRegex(item, args);
+
+        // determine if the row matches the selected tags
+        var matchesTags = true;
+        if (matchesFilter) {
+            var tagFilters = $('#controller-service-tag-cloud').tagcloud('getSelectedTags');
+            var hasSelectedTags = tagFilters.length > 0;
+            if (hasSelectedTags) {
+                matchesTags = matchesSelectedTags(tagFilters, item['tags']);
+            }
+        }
+
+        // determine if this row should be visible
+        var matches = matchesFilter && matchesTags;
+
+        // if this row is currently selected and its being filtered
+        if (matches === false && $('#selected-controller-service-type').text() === item['type']) {
+            clearControllerServiceSelection();
+        }
+
+        // update visibility flag
+        item.visible = matches;
+
+        return matches;
+    };
+
+    /**
+     * Determines if the item matches the filter.
+     * 
+     * @param {object} item     The item to filter
+     * @param {object} args     The filter criteria
+     * @returns {boolean}       Whether the item matches the filter
+     */
+    var matchesRegex = function (item, args) {
+        if (args.searchString === '') {
+            return true;
+        }
+
+        try {
+            // perform the row filtering
+            var filterExp = new RegExp(args.searchString, 'i');
+        } catch (e) {
+            // invalid regex
+            return false;
+        }
+
+        // determine if the item matches the filter
+        return item[args.property].search(filterExp) >= 0;
+    };
+
+    /**
+     * Determines if the specified tags match all the tags selected by the user.
+     * 
+     * @argument {string[]} tagFilters      The tag filters
+     * @argument {string} tags              The tags to test
+     */
+    var matchesSelectedTags = function (tagFilters, tags) {
+        var selectedTags = [];
+        $.each(tagFilters, function (_, filter) {
+            selectedTags.push(filter);
+        });
+
+        // normalize the tags
+        var normalizedTags = tags.toLowerCase();
+
+        var matches = true;
+        $.each(selectedTags, function (i, selectedTag) {
+            if (normalizedTags.indexOf(selectedTag) === -1) {
+                matches = false;
+                return false;
+            }
+        });
+
+        return matches;
+    };
+
+    /**
+     * Formats the type by introducing expand/collapse where appropriate.
+     * 
+     * @param {type} row
+     * @param {type} cell
+     * @param {type} value
+     * @param {type} columnDef
+     * @param {type} dataContext
+     */
+    var expandableTypeFormatter = function (row, cell, value, columnDef, dataContext) {
+        var markup = '';
+
+        var indent = 0;
+        var documentedType = dataContext;
+        while (documentedType.parent !== null) {
+            indent += 20;
+            documentedType = documentedType.parent;
+        }
+
+        var padding = 3;
+
+        // create the markup for the row
+        if (dataContext.children.length > 0) {
+            // determine how to render the expansion button
+            var expansionStyle = 'expanded';
+            if (dataContext.collapsed === true) {
+                expansionStyle = 'collapsed';
+            }
+
+            // calculate the number of visible/total children
+            var visibleChildren = 0;
+            var totalChildren = 0;
+            var countChildren = function (item) {
+                $.each(item.children, function (_, child) {
+                    if (child.children.length > 0) {
+                        countChildren(child);
+                    } else {
+                        if (child.visible) {
+                            visibleChildren++;
+                        }
+                        totalChildren++;
+                    }
+                });
+            };
+            countChildren(dataContext);
+
+            markup += ('<span style="margin-top: 5px; margin-left: ' + indent + 'px;" class="expansion-button ' + expansionStyle + '"></span><span class="ancestor-type" style="margin-left: ' + padding + 'px;">' + value + '</span><span class="ancestor-type-rollup">(' + visibleChildren + ' of ' + totalChildren + ')</span>');
+        } else {
+            if (dataContext.parent === null) {
+                padding = 0;
+            }
+
+            markup += ('<span style="margin-left: ' + (indent + padding) + 'px;">' + value + '</span>');
+        }
+
+        return markup;
+    };
+
+    /**
+     * Adds a new controller service of the specified type.
+     * 
+     * @param {string} controllerServiceType
+     */
+    var addControllerService = function (controllerServiceType) {
+        var revision = nf.Client.getRevision();
+
+        // get the desired availability
+        var availability;
+        if (nf.Canvas.isClustered()) {
+            availability = $('#controller-service-availability-combo').combo('getSelectedOption').value;
+        } else {
+            availability = config.node;
+        }
+
+        // add the new controller service
+        var addService = $.ajax({
+            type: 'POST',
+            url: config.urls.controllerServices + '/' + encodeURIComponent(availability),
+            data: {
+                version: revision.version,
+                clientId: revision.clientId,
+                type: controllerServiceType
+            },
+            dataType: 'json'
+        }).done(function (response) {
+            // update the revision
+            nf.Client.setRevision(response.revision);
+
+            // add the item
+            var controllerService = response.controllerService;
+            var controllerServicesGrid = $('#controller-services-table').data('gridInstance');
+            var controllerServicesData = controllerServicesGrid.getData();
+            controllerServicesData.addItem(controllerService);
+
+            // resort
+            controllerServicesData.reSort();
+            controllerServicesGrid.invalidate();
+
+            // select the new controller service
+            var row = controllerServicesData.getRowById(controllerService.id);
+            controllerServicesGrid.setSelectedRows([row]);
+        }).fail(nf.Common.handleAjaxError);
+
+        // hide the dialog
+        $('#new-controller-service-dialog').modal('hide');
+
+        return addService;
+    };
+
+    /**
+     * Initializes the new controller service dialog.
+     */
+    var initNewControllerServiceDialog = function () {
+        // specify the combo options
+        $('#controller-service-type-filter-options').combo({
+            options: [{
+                    text: 'by type',
+                    value: 'label'
+                }, {
+                    text: 'by tag',
+                    value: 'tags'
+                }],
+            select: function (option) {
+                applyControllerServiceTypeFilter();
+            }
+        });
+
+        // specify the controller service availability
+        if (nf.Canvas.isClustered()) {
+            $('#controller-service-availability-combo').combo({
+                options: [{
+                        text: 'Node',
+                        value: config.node,
+                        description: 'This controller service will be available on the nodes only.'
+                    }, {
+                        text: 'Cluster Manager',
+                        value: config.ncm,
+                        description: 'This controller service will be available on the cluster manager only.'
+                    }]
+            });
+            $('#controller-service-availability-container').show();
+        }
+
+        // define the function for filtering the list
+        $('#controller-service-type-filter').keyup(function () {
+            applyControllerServiceTypeFilter();
+        }).focus(function () {
+            if ($(this).hasClass(config.styles.filterList)) {
+                $(this).removeClass(config.styles.filterList).val('');
+            }
+        }).blur(function () {
+            if ($(this).val() === '') {
+                $(this).addClass(config.styles.filterList).val(config.filterText);
+            }
+        }).addClass(config.styles.filterList).val(config.filterText);
+
+        // initialize the processor type table
+        var controllerServiceTypesColumns = [
+            {id: 'type', name: 'Type', field: 'label', formatter: expandableTypeFormatter, sortable: false, resizable: true},
+            {id: 'tags', name: 'Tags', field: 'tags', sortable: false, resizable: true}
+        ];
+
+        // initialize the dataview
+        var controllerServiceTypesData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+        controllerServiceTypesData.setItems([]);
+        controllerServiceTypesData.setFilterArgs({
+            searchString: getControllerServiceTypeFilterText(),
+            property: $('#controller-service-type-filter-options').combo('getSelectedOption').value
+        });
+        controllerServiceTypesData.setFilter(filterControllerServiceTypes);
+        controllerServiceTypesData.getItemMetadata = function (index) {
+            var item = controllerServiceTypesData.getItem(index);
+            if (item && item.children.length > 0) {
+                return {
+                    selectable: false,
+                    columns: {
+                        0: {
+                            colspan: '*'
+                        }
+                    }
+                };
+            } else {
+                return {};
+            }
+        };
+
+        var getVisibleControllerServiceCount = function () {
+            var count = 0;
+            for (var i = 0; i < controllerServiceTypesData.getLength(); i++) {
+                var item = controllerServiceTypesData.getItem(i);
+                if (item.children.length === 0) {
+                    count++;
+                }
+            }
+            return count;
+        };
+
+        // initialize the grid
+        var controllerServiceTypesGrid = new Slick.Grid('#controller-service-types-table', controllerServiceTypesData, controllerServiceTypesColumns, gridOptions);
+        controllerServiceTypesGrid.setSelectionModel(new Slick.RowSelectionModel());
+        controllerServiceTypesGrid.registerPlugin(new Slick.AutoTooltips());
+        controllerServiceTypesGrid.setSortColumn('type', true);
+        controllerServiceTypesGrid.onSelectedRowsChanged.subscribe(function (e, args) {
+            if ($.isArray(args.rows) && args.rows.length === 1) {
+                var controllerServiceTypeIndex = args.rows[0];
+                var controllerServiceType = controllerServiceTypesGrid.getDataItem(controllerServiceTypeIndex);
+
+                // only allow selection of service implementations
+                if (controllerServiceType.children.length === 0) {
+                    // set the controller service type description
+                    if (nf.Common.isBlank(controllerServiceType.description)) {
+                        $('#controller-service-type-description').attr('title', '').html('<span class="unset">No description specified</span>');
+                    } else {
+                        $('#controller-service-type-description').html(controllerServiceType.description).ellipsis();
+                    }
+
+                    // populate the dom
+                    $('#controller-service-type-name').text(controllerServiceType.label).ellipsis();
+                    $('#selected-controller-service-name').text(controllerServiceType.label);
+                    $('#selected-controller-service-type').text(controllerServiceType.type);
+
+                    // show the selected controller service
+                    $('#controller-service-description-container').show();
+                }
+            }
+        });
+        controllerServiceTypesGrid.onClick.subscribe(function (e, args) {
+            var item = controllerServiceTypesData.getItem(args.row);
+            if (item && item.children.length > 0) {
+                // update the grid
+                item.collapsed = !item.collapsed;
+                controllerServiceTypesData.updateItem(item.id, item);
+
+                // update any affected ancestors
+                var parent = item.parent;
+                while (parent !== null) {
+                    controllerServiceTypesData.updateItem(parent.id, parent);
+                    parent = parent.parent;
+                }
+
+                // prevent selection within slickgrid
+                e.stopImmediatePropagation();
+            }
+        });
+        controllerServiceTypesGrid.onDblClick.subscribe(function (e, args) {
+            var controllerServiceType = controllerServiceTypesGrid.getDataItem(args.row);
+            addControllerService(controllerServiceType.type);
+        });
+
+        // wire up the dataview to the grid
+        controllerServiceTypesData.onRowCountChanged.subscribe(function (e, args) {
+            controllerServiceTypesGrid.updateRowCount();
+            controllerServiceTypesGrid.render();
+
+            // update the total number of displayed processors
+            $('#displayed-controller-service-types').text(getVisibleControllerServiceCount());
+        });
+        controllerServiceTypesData.onRowsChanged.subscribe(function (e, args) {
+            controllerServiceTypesGrid.invalidateRows(args.rows);
+            controllerServiceTypesGrid.render();
+        });
+        controllerServiceTypesData.syncGridSelection(controllerServiceTypesGrid, true);
+
+        // hold onto an instance of the grid
+        $('#controller-service-types-table').data('gridInstance', controllerServiceTypesGrid);
+
+        // load the available controller services
+        $.ajax({
+            type: 'GET',
+            url: config.urls.controllerServiceTypes,
+            dataType: 'json'
+        }).done(function (response) {
+            var id = 0;
+            var tags = [];
+
+            // begin the update
+            controllerServiceTypesData.beginUpdate();
+
+            var addType = function (parentItem, documentedType) {
+                var item = {
+                    id: id++,
+                    label: nf.Common.substringAfterLast(documentedType.type, '.'),
+                    type: documentedType.type,
+                    description: nf.Common.escapeHtml(documentedType.description),
+                    tags: documentedType.tags.join(', '),
+                    parent: parentItem,
+                    children: [],
+                    collapsed: false,
+                    visible: true
+                };
+
+                // add the documented type
+                controllerServiceTypesData.addItem(item);
+
+                // count the frequency of each tag for this type
+                $.each(documentedType.tags, function (i, tag) {
+                    tags.push(tag.toLowerCase());
+                });
+
+                // add each of its children
+                $.each(documentedType.childTypes, function (_, documentedChildType) {
+                    var childItem = addType(item, documentedChildType);
+                    item.children.push(childItem);
+                });
+
+                return item;
+            };
+
+            // go through each controller service type
+            $.each(response.controllerServiceTypes, function (i, documentedType) {
+                addType(null, documentedType);
+            });
+
+            // end the udpate
+            controllerServiceTypesData.endUpdate();
+            
+            // set the total number of processors
+            $('#total-controller-service-types, #displayed-controller-service-types').text(getVisibleControllerServiceCount());
+
+            // create the tag cloud
+            $('#controller-service-tag-cloud').tagcloud({
+                tags: tags,
+                select: applyControllerServiceTypeFilter,
+                remove: applyControllerServiceTypeFilter
+            });
+        }).fail(nf.Common.handleAjaxError);
+
+        // initialize the controller service dialog
+        $('#new-controller-service-dialog').modal({
+            headerText: 'Add Controller Service',
+            overlayBackground: false,
+            buttons: [{
+                    buttonText: 'Add',
+                    handler: {
+                        click: function () {
+                            var selectedServiceType = $('#selected-controller-service-type').text();
+                            
+                            // ensure something was selected
+                            if (selectedServiceType === '') {
+                                nf.Dialog.showOkDialog({
+                                    dialogContent: 'The type of controller service to create must be selected.',
+                                    overlayBackground: false
+                                });
+                            } else {
+                                addControllerService(selectedServiceType);
+                            }
+                        }
+                    }
+                }, {
+                    buttonText: 'Cancel',
+                    handler: {
+                        click: function () {
+                            $(this).modal('hide');
+                        }
+                    }
+                }],
+            handler: {
+                close: function () {
+                    // clear the selected row
+                    clearSelectedControllerService();
+
+                    // clear any filter strings
+                    $('#controller-service-type-filter').addClass(config.styles.filterList).val(config.filterText);
+
+                    // clear the tagcloud
+                    $('#controller-service-tag-cloud').tagcloud('clearSelectedTags');
+
+                    // reset the filter
+                    applyControllerServiceTypeFilter();
+
+                    // unselect any current selection
+                    var processTypesGrid = $('#controller-service-types-table').data('gridInstance');
+                    processTypesGrid.setSelectedRows([]);
+                    processTypesGrid.resetActiveCell();
+                }
+            }
+        }).draggable({
+            containment: 'parent',
+            handle: '.dialog-header'
+        });
+    };
+
+    /**
+     * Formatter for the type column.
+     * 
+     * @param {type} row
+     * @param {type} cell
+     * @param {type} value
+     * @param {type} columnDef
+     * @param {type} dataContext
+     * @returns {String}
+     */
+    var typeFormatter = function (row, cell, value, columnDef, dataContext) {
+        return nf.Common.substringAfterLast(value, '.');
+    };
+
+    /**
+     * Formatter for the availability column.
+     * 
+     * @param {type} row
+     * @param {type} cell
+     * @param {type} value
+     * @param {type} columnDef
+     * @param {type} dataContext
+     * @returns {String}
+     */
+    var availabilityFormatter = function (row, cell, value, columnDef, dataContext) {
+        if (value === config.node) {
+            return 'Node';
+        } else {
+            return 'Cluster Manager';
+        }
+    };
+
+    /**
+     * Sorts the specified data using the specified sort details.
+     * 
+     * @param {object} sortDetails
+     * @param {object} data
+     */
+    var sort = function (sortDetails, data) {
+        // defines a function for sorting
+        var comparer = function (a, b) {
+            var aString = nf.Common.isDefinedAndNotNull(a[sortDetails.columnId]) ? a[sortDetails.columnId] : '';
+            var bString = nf.Common.isDefinedAndNotNull(b[sortDetails.columnId]) ? b[sortDetails.columnId] : '';
+            return aString === bString ? 0 : aString > bString ? 1 : -1;
+        };
+
+        // perform the sort
+        data.sort(comparer, sortDetails.sortAsc);
+    };
+
+    /**
+     * Initializes the controller services tab.
+     */
+    var initControllerServices = function () {
+        // initialize the new controller service dialog
+        initNewControllerServiceDialog();
+
+        // more details formatter
+        var moreControllerServiceDetails = function (row, cell, value, columnDef, dataContext) {
+            var markup = '<img src="images/iconDetails.png" title="View Details" class="pointer view-controller-service" style="margin-top: 5px; float: left;" />&nbsp;&nbsp;';
+            if (!nf.Common.isEmpty(dataContext.validationErrors)) {
+                markup += '<img src="images/iconAlert.png" class="has-errors" style="margin-top: 4px; float: left;" /><span class="hidden row-id">' + nf.Common.escapeHtml(dataContext.id) + '</span>';
+            }
+            return markup;
+        };
+        
+        var controllerServiceStateFormatter = function (row, cell, value, columnDef, dataContext) {
+            // determine the appropriate label
+            var icon = '', label = '';
+            if (!nf.Common.isEmpty(dataContext.validationErrors)) {
+                icon = 'invalid';
+                label = 'Invalid';
+            } else {
+                if (value === 'DISABLED') {
+                    icon = 'disabled';
+                    label = 'Disabled';
+                } else if (value === 'DISABLING') {
+                    icon = 'disabled';
+                    label = 'Disabling';
+                } else if (value === 'ENABLED') {
+                    icon = 'enabled';
+                    label = 'Enabled';
+                } else if (value === 'ENABLING') {
+                    icon = 'enabled';
+                    label = 'Enabling';
+                }
+            }
+            
+            // format the markup
+            var formattedValue = '<div class="' + icon + '" style="margin-top: 3px;"></div>';
+            return formattedValue + '<div class="status-text" style="margin-top: 2px; margin-left: 4px; float: left;">' + label + '</div>';
+        };
+
+        var controllerServiceActionFormatter = function (row, cell, value, columnDef, dataContext) {
+            var markup = '';
+
+            // only DFMs can edit a controller service
+            if (nf.Common.isDFM()) {
+                if (dataContext.state === 'ENABLED' || dataContext.state === 'ENABLING') {
+                    markup += '<img src="images/iconDisable.png" title="Disable" class="pointer disable-controller-service" style="margin-top: 2px;" />&nbsp;';
+                } else if (dataContext.state === 'DISABLED') {
+                    markup += '<img src="images/iconEdit.png" title="Edit" class="pointer edit-controller-service" style="margin-top: 2px;" />&nbsp;';
+                    markup += '<img src="images/iconEnable.png" title="Enable" class="pointer enable-controller-service" style="margin-top: 2px;"/>&nbsp;';
+                    markup += '<img src="images/iconDelete.png" title="Remove" class="pointer delete-controller-service" style="margin-top: 2px;" />&nbsp;';
+                }
+            }
+
+            // always include a button to view the usage
+            markup += '<img src="images/iconUsage.png" title="Usage" class="pointer controller-service-usage" style="margin-top: 2px;"/>&nbsp;';
+
+            return markup;
+        };
+
+        // define the column model for the controller services table
+        var controllerServicesColumns = [
+            {id: 'moreDetails', name: '&nbsp;', resizable: false, formatter: moreControllerServiceDetails, sortable: false, width: 50, maxWidth: 50},
+            {id: 'name', field: 'name', name: 'Name', sortable: true, resizable: true},
+            {id: 'type', field: 'type', name: 'Type', formatter: typeFormatter, sortable: true, resizable: true},
+            {id: 'state', field: 'state', name: 'State', formatter: controllerServiceStateFormatter, sortable: true, resizeable: true}
+        ];
+
+        // only show availability when clustered
+        if (nf.Canvas.isClustered()) {
+            controllerServicesColumns.push({id: 'availability', field: 'availability', name: 'Availability', formatter: availabilityFormatter, sortable: true, resizeable: true});
+        }
+        
+        // action column should always be last
+        controllerServicesColumns.push({id: 'actions', name: '&nbsp;', resizable: false, formatter: controllerServiceActionFormatter, sortable: false, width: 90, maxWidth: 90});
+
+        // initialize the dataview
+        var controllerServicesData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+        controllerServicesData.setItems([]);
+
+        // initialize the sort
+        sort({
+            columnId: 'name',
+            sortAsc: true
+        }, controllerServicesData);
+
+        // initialize the grid
+        var controllerServicesGrid = new Slick.Grid('#controller-services-table', controllerServicesData, controllerServicesColumns, gridOptions);
+        controllerServicesGrid.setSelectionModel(new Slick.RowSelectionModel());
+        controllerServicesGrid.registerPlugin(new Slick.AutoTooltips());
+        controllerServicesGrid.setSortColumn('name', true);
+        controllerServicesGrid.onSort.subscribe(function (e, args) {
+            sort({
+                columnId: args.sortCol.field,
+                sortAsc: args.sortAsc
+            }, controllerServicesData);
+        });
+
+        // configure a click listener
+        controllerServicesGrid.onClick.subscribe(function (e, args) {
+            var target = $(e.target);
+
+            // get the service at this row
+            var controllerService = controllerServicesData.getItem(args.row);
+
+            // determine the desired action
+            if (controllerServicesGrid.getColumns()[args.cell].id === 'actions') {
+                if (target.hasClass('edit-controller-service')) {
+                    nf.ControllerService.showConfiguration(controllerService);
+                } else if (target.hasClass('enable-controller-service')) {
+                    nf.ControllerService.enable(controllerService);
+                } else if (target.hasClass('disable-controller-service')) {
+                    nf.ControllerService.disable(controllerService);
+                } else if (target.hasClass('delete-controller-service')) {
+                    nf.ControllerService.remove(controllerService);
+                } else if (target.hasClass('controller-service-usage')) {
+                    // close the settings dialog
                     $('#shell-close-button').click();
-                }).fail(function (xhr, status, error) {
-                    // close the details panel
-                    $('#settings-cancel').click();
+                    
+                    // open the documentation for this reporting task
+                    nf.Shell.showPage('../nifi-docs/documentation?' + $.param({
+                        select: nf.Common.substringAfterLast(controllerService.type, '.')
+                    })).done(function() {
+                        nf.Settings.showSettings();
+                    });
+                }
+            } else if (controllerServicesGrid.getColumns()[args.cell].id === 'moreDetails') {
+                if (target.hasClass('view-controller-service')) {
+                    nf.ControllerService.showDetails(controllerService);
+                }
+            }
+        });
+
+        // wire up the dataview to the grid
+        controllerServicesData.onRowCountChanged.subscribe(function (e, args) {
+            controllerServicesGrid.updateRowCount();
+            controllerServicesGrid.render();
+        });
+        controllerServicesData.onRowsChanged.subscribe(function (e, args) {
+            controllerServicesGrid.invalidateRows(args.rows);
+            controllerServicesGrid.render();
+        });
+        controllerServicesData.syncGridSelection(controllerServicesGrid, true);
+
+        // hold onto an instance of the grid
+        $('#controller-services-table').data('gridInstance', controllerServicesGrid).on('mouseenter', 'div.slick-cell', function (e) {
+            var errorIcon = $(this).find('img.has-errors');
+            if (errorIcon.length && !errorIcon.data('qtip')) {
+                var serviceId = $(this).find('span.row-id').text();
+
+                // get the service item
+                var item = controllerServicesData.getItemById(serviceId);
+
+                // format the errors
+                var tooltip = nf.Common.formatUnorderedList(item.validationErrors);
+
+                // show the tooltip
+                if (nf.Common.isDefinedAndNotNull(tooltip)) {
+                    errorIcon.qtip($.extend({
+                        content: tooltip,
+                        position: {
+                            target: 'mouse',
+                            viewport: $(window),
+                            adjust: {
+                                x: 8,
+                                y: 8,
+                                method: 'flipinvert flipinvert'
+                            }
+                        }
+                    }, nf.Common.config.tooltipConfig));
+                }
+            }
+        });
+    };
 
-                    // handle the error
-                    nf.Common.handleAjaxError(xhr, status, error);
+    /**
+     * Loads the controller services.
+     */
+    var loadControllerServices = function () {
+        var services = [];
+
+        // get the controller services that are running on the nodes
+        var nodeControllerServices = $.ajax({
+            type: 'GET',
+            url: config.urls.controllerServices + '/' + encodeURIComponent(config.node),
+            dataType: 'json'
+        }).done(function (response) {
+            var nodeServices = response.controllerServices;
+            if (nf.Common.isDefinedAndNotNull(nodeServices)) {
+                $.each(nodeServices, function (_, nodeService) {
+                    services.push(nodeService);
                 });
+            }
+        });
+
+        // get the controller services that are running on the ncm
+        var ncmControllerServices = $.Deferred(function (deferred) {
+            if (nf.Canvas.isClustered()) {
+                $.ajax({
+                    type: 'GET',
+                    url: config.urls.controllerServices + '/' + encodeURIComponent(config.ncm),
+                    dataType: 'json'
+                }).done(function (response) {
+                    var ncmServices = response.controllerServices;
+                    if (nf.Common.isDefinedAndNotNull(ncmServices)) {
+                        $.each(ncmServices, function (_, ncmService) {
+                            services.push(ncmService);
+                        });
+                    }
+                    deferred.resolve();
+                }).fail(function () {
+                    deferred.reject();
+                });
+            } else {
+                deferred.resolve();
+            }
+        }).promise();
+
+        // add all controller services
+        return $.when(nodeControllerServices, ncmControllerServices).done(function () {
+            var controllerServicesElement = $('#controller-services-table');
+            nf.Common.cleanUpTooltips(controllerServicesElement, 'img.has-errors');
+
+            var controllerServicesGrid = controllerServicesElement.data('gridInstance');
+            var controllerServicesData = controllerServicesGrid.getData();
+
+            // update the controller services
+            controllerServicesData.setItems(services);
+            controllerServicesData.reSort();
+            controllerServicesGrid.invalidate();
+        });
+    };
+
+    /**
+     * Get the text out of the filter field. If the filter field doesn't
+     * have any text it will contain the text 'filter list' so this method
+     * accounts for that.
+     */
+    var getReportingTaskTypeFilterText = function () {
+        var filterText = '';
+        var filterField = $('#reporting-task-type-filter');
+        if (!filterField.hasClass(config.styles.filterList)) {
+            filterText = filterField.val();
+        }
+        return filterText;
+    };
+
+    /**
+     * Filters the reporting task type table.
+     */
+    var applyReportingTaskTypeFilter = function () {
+        // get the dataview
+        var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance');
+
+        // ensure the grid has been initialized
+        if (nf.Common.isDefinedAndNotNull(reportingTaskTypesGrid)) {
+            var reportingTaskTypesData = reportingTaskTypesGrid.getData();
+
+            // update the search criteria
+            reportingTaskTypesData.setFilterArgs({
+                searchString: getReportingTaskTypeFilterText(),
+                property: $('#reporting-task-type-filter-options').combo('getSelectedOption').value
+            });
+
+            // need to invalidate the entire table since parent elements may need to be 
+            // rerendered due to changes in their children
+            reportingTaskTypesData.refresh();
+            reportingTaskTypesGrid.invalidate();
+        }
+    };
+
+    /**
+     * Hides the selected reporting task.
+     */
+    var clearSelectedReportingTask = function () {
+        if (nf.Canvas.isClustered()) {
+            $('#reporting-task-availability-combo').combo('setSelectedOption', {
+                value: config.node
             });
+        }
+        
+        $('#reporting-task-type-description').text('');
+        $('#reporting-task-type-name').text('');
+        $('#selected-reporting-task-name').text('');
+        $('#selected-reporting-task-type').text('');
+        $('#reporting-task-description-container').hide();
+    };
+
+    /**
+     * Clears the selected reporting task type.
+     */
+    var clearReportingTaskSelection = function () {
+        // clear the selected row
+        clearSelectedReportingTask();
+
+        // clear the active cell the it can be reselected when its included
+        var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance');
+        reportingTaskTypesGrid.resetActiveCell();
+    };
+
+    /**
+     * Performs the filtering.
+     * 
+     * @param {object} item     The item subject to filtering
+     * @param {object} args     Filter arguments
+     * @returns {Boolean}       Whether or not to include the item
+     */
+    var filterReportingTaskTypes = function (item, args) {
+        // determine if the item matches the filter
+        var matchesFilter = matchesRegex(item, args);
+
+        // determine if the row matches the selected tags
+        var matchesTags = true;
+        if (matchesFilter) {
+            var tagFilters = $('#reporting-task-tag-cloud').tagcloud('getSelectedTags');
+            var hasSelectedTags = tagFilters.length > 0;
+            if (hasSelectedTags) {
+                matchesTags = matchesSelectedTags(tagFilters, item['tags']);
+            }
+        }
+
+        // determine if this row should be visible
+        var matches = matchesFilter && matchesTags;
+
+        // if this row is currently selected and its being filtered
+        if (matches === false && $('#selected-reporting-task-type').text() === item['type']) {
+            clearReportingTaskSelection();
+        }
+
+        // update visibility flag
+        item.visible = matches;
+
+        return matches;
+    };
 
-            // install a cancel button listener to close the shell
-            $('#settings-cancel').click(function () {
-                $('#shell-close-button').click();
+    /**
+     * Adds a new reporting task of the specified type.
+     * 
+     * @param {string} reportingTaskType
+     */
+    var addReportingTask = function (reportingTaskType) {
+        var revision = nf.Client.getRevision();
+
+        // get the desired availability
+        var availability;
+        if (nf.Canvas.isClustered()) {
+            availability = $('#reporting-task-availability-combo').combo('getSelectedOption').value;
+        } else {
+            availability = config.node;
+        }
+
+        // add the new reporting task
+        var addTask = $.ajax({
+            type: 'POST',
+            url: config.urls.reportingTasks + '/' + encodeURIComponent(availability),
+            data: {
+                version: revision.version,
+                clientId: revision.clientId,
+                type: reportingTaskType
+            },
+            dataType: 'json'
+        }).done(function (response) {
+            // update the revision
+            nf.Client.setRevision(response.revision);
+
+            // add the item
+            var reportingTask = response.reportingTask;
+            var reportingTaskGrid = $('#reporting-tasks-table').data('gridInstance');
+            var reportingTaskData = reportingTaskGrid.getData();
+            reportingTaskData.addItem(reportingTask);
+
+            // resort
+            reportingTaskData.reSort();
+            reportingTaskGrid.invalidate();
+
+            // select the new reporting task
+            var row = reportingTaskData.getRowById(reportingTask.id);
+            reportingTaskGrid.setSelectedRows([row]);
+        }).fail(nf.Common.handleAjaxError);
+
+        // hide the dialog
+        $('#new-reporting-task-dialog').modal('hide');
+
+        return addTask;
+    };
+
+    /**
+     * Initializes the new reporting task dialog.
+     */
+    var initNewReportingTaskDialog = function () {
+        // specify the combo options
+        $('#reporting-task-type-filter-options').combo({
+            options: [{
+                    text: 'by type',
+                    value: 'label'
+                }, {
+                    text: 'by tag',
+                    value: 'tags'
+                }],
+            select: function (option) {
+                applyReportingTaskTypeFilter();
+            }
+        });
+
+        // specify the reporting task availability
+        if (nf.Canvas.isClustered()) {
+            $('#reporting-task-availability-combo').combo({
+                options: [{
+                        text: 'Node',
+                        value: config.node,
+                        description: 'This reporting task will be available on the nodes only.'
+                    }, {
+                        text: 'Cluster Manager',
+                        value: config.ncm,
+                        description: 'This reporting task will be available on the cluster manager only.'
+                    }]
             });
+            $('#reporting-task-availability-container').show();
+        }
+
+        // define the function for filtering the list
+        $('#reporting-task-type-filter').keyup(function () {
+            applyReportingTaskTypeFilter();
+        }).focus(function () {
+            if ($(this).hasClass(config.styles.filterList)) {
+                $(this).removeClass(config.styles.filterList).val('');
+            }
+        }).blur(function () {
+            if ($(this).val() === '') {
+                $(this).addClass(config.styles.filterList).val(config.filterText);
+            }
+        }).addClass(config.styles.filterList).val(config.filterText);
+
+        // initialize the processor type table
+        var reportingTaskTypesColumns = [
+            {id: 'type', name: 'Type', field: 'label', sortable: false, resizable: true},
+            {id: 'tags', name: 'Tags', field: 'tags', sortable: false, resizable: true}
+        ];
+
+        // initialize the dataview
+        var reportingTaskTypesData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+        reportingTaskTypesData.setItems([]);
+        reportingTaskTypesData.setFilterArgs({
+            searchString: getReportingTaskTypeFilterText(),
+            property: $('#reporting-task-type-filter-options').combo('getSelectedOption').value
+        });
+        reportingTaskTypesData.setFilter(filterReportingTaskTypes);
+
+        // initialize the grid
+        var reportingTaskTypesGrid = new Slick.Grid('#reporting-task-types-table', reportingTaskTypesData, reportingTaskTypesColumns, gridOptions);
+        reportingTaskTypesGrid.setSelectionModel(new Slick.RowSelectionModel());
+        reportingTaskTypesGrid.registerPlugin(new Slick.AutoTooltips());
+        reportingTaskTypesGrid.setSortColumn('type', true);
+        reportingTaskTypesGrid.onSelectedRowsChanged.subscribe(function (e, args) {
+            if ($.isArray(args.rows) && args.rows.length === 1) {
+                var reportingTaskTypeIndex = args.rows[0];
+                var reportingTaskType = reportingTaskTypesGrid.getDataItem(reportingTaskTypeIndex);
+
+                // set the reporting task type description
+                if (nf.Common.isBlank(reportingTaskType.description)) {
+                    $('#reporting-task-type-description').attr('title', '').html('<span class="unset">No description specified</span>');
+                } else {
+                    $('#reporting-task-type-description').html(reportingTaskType.description).ellipsis();
+                }
+
+                // populate the dom
+                $('#reporting-task-type-name').text(reportingTaskType.label).ellipsis();
+                $('#selected-reporting-task-name').text(reportingTaskType.label);
+                $('#selected-reporting-task-type').text(reportingTaskType.type);
+
+                // show the selected reporting task
+                $('#reporting-task-description-container').show();
+            }
+        });
+        reportingTaskTypesGrid.onDblClick.subscribe(function (e, args) {
+            var reportingTaskType = reportingTaskTypesGrid.getDataItem(args.row);
+            addReportingTask(reportingTaskType.type);
+        });
+
+        // wire up the dataview to the grid
+        reportingTaskTypesData.onRowCountChanged.subscribe(function (e, args) {
+            reportingTaskTypesGrid.updateRowCount();
+            reportingTaskTypesGrid.render();
+
+            // update the total number of displayed processors
+            $('#displayed-reporting-task-types').text(args.current);
+        });
+        reportingTaskTypesData.onRowsChanged.subscribe(function (e, args) {
+            reportingTaskTypesGrid.invalidateRows(args.rows);
+            reportingTaskTypesGrid.render();
+        });
+        reportingTaskTypesData.syncGridSelection(reportingTaskTypesGrid, true);
+
+        // hold onto an instance of the grid
+        $('#reporting-task-types-table').data('gridInstance', reportingTaskTypesGrid);
+
+        // load the available reporting tasks
+        $.ajax({
+            type: 'GET',
+            url: config.urls.reportingTaskTypes,
+            dataType: 'json'
+        }).done(function (response) {
+            var id = 0;
+            var tags = [];
+
+            // begin the update
+            reportingTaskTypesData.beginUpdate();
+
+            // go through each reporting task type
+            $.each(response.reportingTaskTypes, function (i, documentedType) {
+                // add the documented type
+                reportingTaskTypesData.addItem({
+                    id: id++,
+                    label: nf.Common.substringAfterLast(documentedType.type, '.'),
+                    type: documentedType.type,
+                    description: nf.Common.escapeHtml(documentedType.description),
+                    tags: documentedType.tags.join(', '),
+                    children: [],
+                    collapsed: false,
+                    visible: true
+                });
+
+                // count the frequency of each tag for this type
+                $.each(documentedType.tags, function (i, tag) {
+                    tags.push(tag.toLowerCase());
+                });
+            });
+
+            // end the udpate
+            reportingTaskTypesData.endUpdate();
+
+            // set the total number of processors
+            $('#total-reporting-task-types, #displayed-reporting-task-types').text(response.reportingTaskTypes.length);
+
+            // create the tag cloud
+            $('#reporting-task-tag-cloud').tagcloud({
+                tags: tags,
+                select: applyReportingTaskTypeFilter,
+                remove: applyReportingTaskTypeFilter
+            });
+        }).fail(nf.Common.handleAjaxError);
+
+        // initialize the reporting task dialog
+        $('#new-reporting-task-dialog').modal({
+            headerText: 'Add Reporting Task',
+            overlayBackground: false,
+            buttons: [{
+                    buttonText: 'Add',
+                    handler: {
+                        click: function () {
+                            var selectedTaskType = $('#selected-reporting-task-type').text();
+                            
+                            // ensure something was selected
+                            if (selectedTaskType === '') {
+                                nf.Dialog.showOkDialog({
+                                    dialogContent: 'The type of reporting task to create must be selected.',
+                                    overlayBackground: false
+                                });
+                            } else {
+                                addReportingTask(selectedTaskType);
+                            }
+                        }
+                    }
+                }, {
+                    buttonText: 'Cancel',
+                    handler: {
+                        click: function () {
+                            $(this).modal('hide');
+                        }
+                    }
+                }],
+            handler: {
+                close: function () {
+                    // clear the selected row
+                    clearSelectedReportingTask();
+
+                    // clear any filter strings
+                    $('#reporting-task-type-filter').addClass(config.styles.filterList).val(config.filterText);
+
+                    // clear the tagcloud
+                    $('#reporting-task-tag-cloud').tagcloud('clearSelectedTags');
+
+                    // reset the filter
+                    applyReportingTaskTypeFilter();
+
+                    // unselect any current selection
+                    var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance');
+                    reportingTaskTypesGrid.setSelectedRows([]);
+                    reportingTaskTypesGrid.resetActiveCell();
+                }
+            }
+        }).draggable({
+            containment: 'parent',
+            handle: '.dialog-header'
+        });
+    };
+
+    /**
+     * Initializes the reporting tasks tab.
+     */
+    var initReportingTasks = function () {
+        // initialize the new reporting task dialog
+        initNewReportingTaskDialog();
+
+        var moreReportingTaskDetails = function (row, cell, value, columnDef, dataContext) {
+            var markup = '<img src="images/iconDetails.png" title="View Details" class="pointer view-reporting-task" style="margin-top: 5px; float: left;" />&nbsp;&nbsp;';
+            if (!nf.Common.isEmpty(dataContext.validationErrors)) {
+                markup += '<img src="images/iconAlert.png" class="has-errors" style="margin-top: 4px; float: left;" /><span class="hidden row-id">' + nf.Common.escapeHtml(dataContext.id) + '</span>';
+            }
+            return markup;
+        };
+        
+        var reportingTaskRunStatusFormatter = function (row, cell, value, columnDef, dataContext) {
+            // determine the appropriate label
+            var label;
+            if (!nf.Common.isEmpty(dataContext.validationErrors)) {
+                label = 'Invalid';
+            } else {
+                if (value === 'STOPPED') {
+                    label = 'Stopped';
+                } else if (value === 'RUNNING') {
+                    label = 'Running';
+                } else {
+                    label = 'Disabled';
+                }
+            }
+            
+            // include the active thread count if appropriate
+            var activeThreadCount = '';
+            if (nf.Common.isDefinedAndNotNull(dataContext.activeThreadCount) && dataContext.activeThreadCount > 0) {
+                activeThreadCount = '(' + dataContext.activeThreadCount + ')';
+            }
+            
+            // format the markup
+            var formattedValue = '<div class="' + nf.Common.escapeHtml(label.toLowerCase()) + '" style="margin-top: 3px;"></div>';
+            return formattedValue + '<div class="status-text" style="margin-top: 2px; margin-left: 4px; float: left;">' + nf.Common.escapeHtml(label) + '</div><div style="float: left; margin-left: 4px;">' + nf.Common.escapeHtml(activeThreadCount) + '</div>';
+        };
+        
+        var reportingTaskActionFormatter = function (row, cell, value, columnDef, dataContext) {
+            var markup = '';
+
+            // only DFMs can edit reporting tasks
+            if (nf.Common.isDFM()) {
+                if (dataContext.state === 'RUNNING') {
+                    markup += '<img src="images/iconStop.png" title="Stop" class="pointer stop-reporting-task" style="margin-top: 2px;" />&nbsp;';
+                } else if (dataContext.state === 'STOPPED' || dataContext.state === 'DISABLED') {
+                    markup += '<img src="images/iconEdit.png" title="Edit" class="pointer edit-reporting-task" style="margin-top: 2px;" />&nbsp;';
+                    markup += '<img src="images/iconRun.png" title="Start" class="pointer start-reporting-task" style="margin-top: 2px;"/>&nbsp;';
+                    markup += '<img src="images/iconDelete.png" title="Remove" class="pointer delete-reporting-task" style="margin-top: 2px;" />&nbsp;';
+                }
+            }
+
+            // always include a button to view the usage
+            markup += '<img src="images/iconUsage.png" title="Usage" class="pointer reporting-task-usage" style="margin-top: 2px;"/>&nbsp;';
+
+            return markup;
+        };
+
+        // define the column model for the reporting tasks table
+        var reportingTasksColumnModel = [
+            {id: 'moreDetails', field: 'moreDetails', name: '&nbsp;', resizable: false, formatter: moreReportingTaskDetails, sortable: true, width: 50, maxWidth: 50},
+            {id: 'name', field: 'name', name: 'Name', sortable: true, resizable: true},
+            {id: 'type', field: 'type', name: 'Type', sortable: true, resizable: true, formatter: typeFormatter},
+            {id: 'state', field: 'state', name: 'Run Status', sortable: true, resizeable: true, formatter: reportingTaskRunStatusFormatter}
+        ];
+
+        // only show availability when clustered
+        if (nf.Canvas.isClustered()) {
+            reportingTasksColumnModel.push({id: 'availability', field: 'availability', name: 'Availability', formatter: availabilityFormatter, sortable: true, resizeable: true});
+        }
+        
+        // action column should always be last
+        reportingTasksColumnModel.push({id: 'actions', name: '&nbsp;', resizable: false, formatter: reportingTaskActionFormatter, sortable: false, width: 90, maxWidth: 90});
+
+        // initialize the dataview
+        var reportingTasksData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+        reportingTasksData.setItems([]);
+
+        // initialize the sort
+        sort({
+            columnId: 'name',
+            sortAsc: true
+        }, reportingTasksData);
+
+        // initialize the grid
+        var reportingTasksGrid = new Slick.Grid('#reporting-tasks-table', reportingTasksData, reportingTasksColumnModel, gridOptions);
+        reportingTasksGrid.setSelectionModel(new Slick.RowSelectionModel());
+        reportingTasksGrid.registerPlugin(new Slick.AutoTooltips());
+        reportingTasksGrid.setSortColumn('name', true);
+        reportingTasksGrid.onSort.subscribe(function (e, args) {
+            sort({
+                columnId: args.sortCol.field,
+                sortAsc: args.sortAsc
+            }, reportingTasksData);
+        });
+
+        // configure a click listener
+        reportingTasksGrid.onClick.subscribe(function (e, args) {
+            var target = $(e.target);
+
+            // get the service at this row
+            var reportingTask = reportingTasksData.getItem(args.row);
+
+            // determine the desired action
+            if (reportingTasksGrid.getColumns()[args.cell].id === 'actions') {
+                if (target.hasClass('edit-reporting-task')) {
+                    nf.ReportingTask.showConfiguration(reportingTask);
+                } else if (target.hasClass('start-reporting-task')) {
+                    nf.ReportingTask.start(reportingTask);
+                } else if (target.hasClass('stop-reporting-task')) {
+                    nf.ReportingTask.stop(reportingTask);
+                } else if (target.hasClass('delete-reporting-task')) {
+                    nf.ReportingTask.remove(reportingTask);
+                } else if (target.hasClass('reporting-task-usage')) {
+                    // close the settings dialog
+                    $('#shell-close-button').click();
+                    
+                    // open the documentation for this reporting task
+                    nf.Shell.showPage('../nifi-docs/documentation?' + $.param({
+                        select: nf.Common.substringAfterLast(reportingTask.type, '.')
+                    })).done(function() {
+                        nf.Settings.showSettings();
+                    });
+                }
+            } else if (reportingTasksGrid.getColumns()[args.cell].id === 'moreDetails') {
+                if (target.hasClass('view-reporting-task')) {
+                    nf.ReportingTask.showDetails(reportingTask);
+                }
+            }
+        });
+
+        // wire up the dataview to the grid
+        reportingTasksData.onRowCountChanged.subscribe(function (e, args) {
+            reportingTasksGrid.updateRowCount();
+            reportingTasksGrid.render();
+        });
+        reportingTasksData.onRowsChanged.subscribe(function (e, args) {
+            reportingTasksGrid.invalidateRows(args.rows);
+            reportingTasksGrid.render();
+        });
+        reportingTasksData.syncGridSelection(reportingTasksGrid, true);
+
+        // hold onto an instance of the grid
+        $('#reporting-tasks-table').data('gridInstance', reportingTasksGrid).on('mouseenter', 'div.slick-cell', function (e) {
+            var errorIcon = $(this).find('img.has-errors');
+            if (errorIcon.length && !errorIcon.data('qtip')) {
+                var taskId = $(this).find('span.row-id').text();
+
+                // get the service item
+                var item = reportingTasksData.getItemById(taskId);
+
+                // format the errors
+                var tooltip = nf.Common.formatUnorderedList(item.validationErrors);
+
+                // show the tooltip
+                if (nf.Common.isDefinedAndNotNull(tooltip)) {
+                    errorIcon.qtip($.extend({
+                        content: tooltip,
+                        position: {
+                            target: 'mouse',
+                            viewport: $(window),
+                            adjust: {
+                                x: 8,
+                                y: 8,
+                                method: 'flipinvert flipinvert'
+                            }
+                        }
+                    }, nf.Common.config.tooltipConfig));
+                }
+            }
+        });
+    };
+
+    /**
+     * Loads the reporting tasks.
+     */
+    var loadReportingTasks = function () {
+        var tasks = [];
+
+        // get the reporting tasks that are running on the nodes
+        var nodeReportingTasks = $.ajax({
+            type: 'GET',
+            url: config.urls.reportingTasks + '/' + encodeURIComponent(config.node),
+            dataType: 'json'
+        }).done(function (response) {
+            var nodeTasks = response.reportingTasks;
+            if (nf.Common.isDefinedAndNotNull(nodeTasks)) {
+                $.each(nodeTasks, function (_, nodeTask) {
+                    tasks.push(nodeTask);
+                });
+            }
+        });
+
+        // get the reporting tasks that are running on the ncm
+        var ncmReportingTasks = $.Deferred(function (deferred) {
+            if (nf.Canvas.isClustered()) {
+                $.ajax({
+                    type: 'GET',
+                    url: config.urls.reportingTasks + '/' + encodeURIComponent(config.ncm),
+                    dataType: 'json'
+                }).done(function (response) {
+                    var ncmTasks = response.reportingTasks;
+                    if (nf.Common.isDefinedAndNotNull(ncmTasks)) {
+                        $.each(ncmTasks, function (_, ncmTask) {
+                            tasks.push(ncmTask);
+                        });
+                    }
+                    deferred.resolve();
+                }).fail(function () {
+                    deferred.reject();
+                });
+            } else {
+                deferred.resolve();
+            }
+        }).promise();
+
+        // add all reporting tasks
+        return $.when(nodeReportingTasks, ncmReportingTasks).done(function () {
+            var reportingTasksElement = $('#reporting-tasks-table');
+            nf.Common.cleanUpTooltips(reportingTasksElement, 'img.has-errors');
+
+            var reportingTasksGrid = reportingTasksElement.data('gridInstance');
+            var reportingTasksData = reportingTasksGrid.getData();
+
+            // update the reporting tasks
+            reportingTasksData.setItems(tasks);
+            reportingTasksData.reSort();
+            reportingTasksGrid.invalidate();
+        });
+    };
+
+    return {
+        /**
+         * Initializes the status page.
+         */
+        init: function () {
+            // initialize the settings tabs
+            $('#settings-tabs').tabbs({
+                tabStyle: 'settings-tab',
+                selectedTabStyle: 'settings-selected-tab',
+                tabs: [{
+                        name: 'General',
+                        tabContentId: 'general-settings-tab-content'
+                    }, {
+                        name: 'Controller Services',
+                        tabContentId: 'controller-services-tab-content'
+                    }, {
+                        name: 'Reporting Tasks',
+                        tabContentId: 'reporting-tasks-tab-content'
+                    }],
+                select: function () {
+                    var tab = $(this).text();
+                    if (tab === 'General') {
+                        $('#new-service-or-task').hide();
+                    } else {
+                        $('#new-service-or-task').show();
+
+                        // update the tooltip on the button
+                        $('#new-service-or-task').attr('title', function () {
+                            if (tab === 'Controller Services') {
+                                return 'Create a new controller service';
+                            } else if (tab === 'Reporting Tasks') {
+                                return 'Create a new reporting task';
+                            }
+                        });
+
+                        // resize the table
+                        nf.Settings.resetTableSize();
+                    }
+                }
+            });
+
+            // setup the tooltip for the refresh icon
+            $('#settings-refresh-required-icon').qtip($.extend({
+                content: 'This flow has been modified by another user. Please refresh.'
+            }, nf.CanvasUtils.config.systemTooltipConfig));
+
+            // refresh the system diagnostics when clicked
+            nf.Common.addHoverEffect('#settings-refresh-button', 'button-refresh', 'button-refresh-hover').click(function () {
+                if ($('#settings-refresh-required-icon').is(':visible')) {
+                    nf.CanvasHeader.reloadAndClearWarnings();
+                } else {
+                    nf.Settings.loadSettings();
+                }
+            });
+
+            // create a new controller service or reporting task
+            $('#new-service-or-task').on('click', function () {
+                var selectedTab = $('li.settings-selected-tab').text();
+                if (selectedTab === 'Controller Services') {
+                    $('#new-controller-service-dialog').modal('show');
+                    
+                    // reset the canvas size after the dialog is shown
+                    var controllerServiceTypesGrid = $('#controller-service-types-table').data('gridInstance');
+                    if (nf.Common.isDefinedAndNotNull(controllerServiceTypesGrid)) {
+                        controllerServiceTypesGrid.resizeCanvas();
+                    }
+                    
+                    // set the initial focus
+                    $('#controller-service-type-filter').focus();
+                } else if (selectedTab === 'Reporting Tasks') {
+                    $('#new-reporting-task-dialog').modal('show');
+                    
+                    // reset the canvas size after the dialog is shown
+                    var reportingTaskTypesGrid = $('#reporting-task-types-table').data('gridInstance');
+                    if (nf.Common.isDefinedAndNotNull(reportingTaskTypesGrid)) {
+                        reportingTaskTypesGrid.resizeCanvas();
+                    }
+                    
+                    // set the initial focus
+                    $('#reporting-task-type-filter').focus();
+                }
+            });
+
+            // initialize each tab
+            initGeneral();
+            initControllerServices();
+            initReportingTasks();
+        },
+        
+        /**
+         * Update the size of the grid based on its container's current size.
+         */
+        resetTableSize: function () {
+            var controllerServicesGrid = $('#controller-services-table').data('gridInstance');
+            if (nf.Common.isDefinedAndNotNull(controllerServicesGrid)) {
+                controllerServicesGrid.resizeCanvas();
+            }
+
+            var reportingTasksGrid = $('#reporting-tasks-table').data('gridInstance');
+            if (nf.Common.isDefinedAndNotNull(reportingTasksGrid)) {
+                reportingTasksGrid.resizeCanvas();
+            }
         },
         
         /**
          * Shows the settings dialog.
          */
         showSettings: function () {
-            $.ajax({
+            // show the settings dialog
+            nf.Shell.showContent('#settings').done(function () {
+                // reset button state
+                $('#settings-save').mouseout();
+            });
+            
+            // adjust the table size
+            nf.Settings.resetTableSize();
+        },
+        
+        /**
+         * Loads the settings.
+         */
+        loadSettings: function () {
+            var settings = $.ajax({
                 type: 'GET',
                 url: config.urls.controllerConfig,
                 dataType: 'json'
@@ -127,20 +1735,31 @@ nf.Settings = (function () {
                 if (nf.Common.isDefinedAndNotNull(response.config)) {
                     // set the header
                     $('#settings-header-text').text(response.config.name + ' Settings');
+                    $('#settings-last-refreshed').text(response.config.currentTime);
 
                     // populate the controller settings
-                    $('#data-flow-title-field').val(response.config.name);
-                    $('#data-flow-comments-field').val(response.config.comments);
-                    $('#maximum-timer-driven-thread-count-field').val(response.config.maxTimerDrivenThreadCount);
-                    $('#maximum-event-driven-thread-count-field').val(response.config.maxEventDrivenThreadCount);
+                    if (nf.Common.isDFM()) {
+                        $('#data-flow-title-field').val(response.config.name);
+                        $('#data-flow-comments-field').val(response.config.comments);
+                        $('#maximum-timer-driven-thread-count-field').val(response.config.maxTimerDrivenThreadCount);
+                        $('#maximum-event-driven-thread-count-field').val(response.config.maxEventDrivenThreadCount);
+                    } else {
+                        $('#read-only-data-flow-title-field').html(nf.Common.formatValue(response.config.name));
+                        $('#read-only-data-flow-comments-field').html(nf.Common.formatValue(response.config.comments));
+                        $('#read-only-maximum-timer-driven-thread-count-field').text(response.config.maxTimerDrivenThreadCount);
+                        $('#read-only-maximum-event-driven-thread-count-field').text(response.config.maxEventDrivenThreadCount);
+                    }
                 }
+            });
 
-                // show the settings dialog
-                nf.Shell.showContent('#settings').done(function () {
-                    // reset button state
-                    $('#settings-save, #settings-cancel').mouseout();
-                });
-            }).fail(nf.Common.handleAjaxError);
+            // load the controller services
+            var controllerServices = loadControllerServices();
+
+            // load the reporting tasks
+            var reportingTasks = loadReportingTasks();
+
+            // return a deferred for all parts of the settings
+            return $.when(settings, controllerServices, reportingTasks).fail(nf.Common.handleAjaxError);
         }
     };
 }());
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-snippet.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-snippet.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-snippet.js
index aa2ecf6..e2e958c 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-snippet.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-snippet.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf, d3 */
+
 nf.Snippet = (function () {
 
     var config = {

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-storage.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-storage.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-storage.js
index 0be05fe..3937970 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-storage.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-storage.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf, d3 */
+
 nf.Storage = (function () {
 
     // Store items for two days before being eligible for removal.

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-toolbar-action.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-toolbar-action.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-toolbar-action.js
index d2cde9a..c3adfd6 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-toolbar-action.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-toolbar-action.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf */
+
 /**
  * Creates a new toolbar action.
  * 

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster-table.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster-table.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster-table.js
index 3ede0e5..74bd958 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster-table.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster-table.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf, Slick */
+
 nf.ClusterTable = (function () {
 
     /**

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster.js
index f6cbdc6..5f00e2c 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf, top */
+
 $(document).ready(function () {
     // initialize the counters page
     nf.Cluster.init();

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/counters/nf-counters-table.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/counters/nf-counters-table.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/counters/nf-counters-table.js
index 28a36bd..5688118 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/counters/nf-counters-table.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/counters/nf-counters-table.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf, Slick */
+
 nf.CountersTable = (function () {
 
     /**

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/counters/nf-counters.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/counters/nf-counters.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/counters/nf-counters.js
index 1f9a861..cbfe336 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/counters/nf-counters.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/counters/nf-counters.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf, top */
+
 $(document).ready(function () {
     // initialize the counters page
     nf.Counters.init();

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history-model.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history-model.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history-model.js
index 0564b17..48194b5 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history-model.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history-model.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global Slick, nf */
+
 (function ($) {
 
     function HistoryModel() {

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history-table.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history-table.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history-table.js
index 72fc549..14c97cf 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history-table.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history-table.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf, Slick */
+
 nf.HistoryTable = (function () {
 
     /**
@@ -356,7 +359,7 @@ nf.HistoryTable = (function () {
 
         // inspect the operation to determine if there are any component details
         if (nf.Common.isDefinedAndNotNull(componentDetails)) {
-            if (action.sourceType === 'Processor') {
+            if (action.sourceType === 'Processor' || action.sourceType === 'ControllerService' || action.sourceType === 'ReportingTask') {
                 detailsMarkup.append(
                         $('<div class="action-detail"><div class="history-details-name">Type</div>' + nf.Common.escapeHtml(componentDetails.type) + '</div>'));
             } else if (action.sourceType === 'RemoteProcessGroup') {
@@ -435,4 +438,4 @@ nf.HistoryTable = (function () {
             historyGrid.onViewportChanged.notify();
         }
     };
-}());
\ No newline at end of file
+}());

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history.js
index 36364df..516507c 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/history/nf-history.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf, top */
+
 $(document).ready(function () {
     // initialize the status page
     nf.History.init();

http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/e9647717/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-client.js
----------------------------------------------------------------------
diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-client.js b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-client.js
index 4cc3035..0d490b7 100644
--- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-client.js
+++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-client.js
@@ -14,6 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+/* global nf */
+
 nf.Client = {};
 
 nf.Client.version = -1;