http://git-wip-us.apache.org/repos/asf/nifi/blob/9152a9fd/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-controller-services.js
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-controller-services.js
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-controller-services.js
new file mode 100644
index 0000000..78e9bca
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-controller-services.js
@@ -0,0 +1,883 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* global nf, Slick, d3 */
+
+nf.ControllerServices = (function () {
+
+    var dblClick = null;
+    var initialized = false;
+
+    var config = {
+        filterText: 'Filter',
+        styles: {
+            filterList: 'filter-list'
+        },
+        urls: {
+            api: '../nifi-api',
+            controllerServiceTypes: '../nifi-api/flow/controller-service-types'
+        }
+    };
+
+    var gridOptions = {
+        forceFitColumns: true,
+        enableTextSelectionOnCells: true,
+        enableCellNavigation: true,
+        enableColumnReorder: false,
+        autoEdit: false,
+        multiSelect: false
+    };
+
+    /**
+     * 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()
+            });
+            controllerServiceTypesData.refresh();
+
+            // update the selection if possible
+            if (controllerServiceTypesData.getLength() > 0) {
+                controllerServiceTypesGrid.setSelectedRows([0]);
+            }
+        }
+    };
+
+    /**
+     * Hides the selected controller service.
+     */
+    var clearSelectedControllerService = function () {
+        $('#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) {
+        // 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();
+        }
+
+        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
+        var matchesLabel = item['label'].search(filterExp) >= 0;
+        var matchesTags = item['tags'].search(filterExp) >= 0;
+        return matchesLabel || matchesTags;
+    };
+
+    /**
+     * 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;
+    };
+
+    /**
+     * Adds the currently selected controller service.
+     *
+     * @param {string} controllerServicesUri
+     * @param {jQuery} serviceTable
+     */
+    var addSelectedControllerService = function (controllerServicesUri, 
serviceTable) {
+        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(controllerServicesUri, serviceTable, 
selectedServiceType);
+        }
+    };
+
+    /**
+     * Adds a new controller service of the specified type.
+     *
+     * @param {string} controllerServicesUri
+     * @param {jQuery} serviceTable
+     * @param {string} controllerServiceType
+     */
+    var addControllerService = function (controllerServicesUri, serviceTable, 
controllerServiceType) {
+        // build the controller service entity
+        var controllerServiceEntity = {
+            'component': {
+                'type': controllerServiceType
+            }
+        };
+
+        // add the new controller service
+        var addService = $.ajax({
+            type: 'POST',
+            url: controllerServicesUri,
+            data: JSON.stringify(controllerServiceEntity),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (controllerServiceEntity) {
+            // add the item
+            var controllerServicesGrid = serviceTable.data('gridInstance');
+            var controllerServicesData = controllerServicesGrid.getData();
+            controllerServicesData.addItem(controllerServiceEntity);
+
+            // resort
+            controllerServicesData.reSort();
+            controllerServicesGrid.invalidate();
+
+            // select the new controller service
+            var row = 
controllerServicesData.getRowById(controllerServiceEntity.id);
+            controllerServicesGrid.setSelectedRows([row]);
+            controllerServicesGrid.scrollRowIntoView(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 () {
+        // define the function for filtering the list
+        $('#controller-service-type-filter').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', 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()
+        });
+        controllerServiceTypesData.setFilter(filterControllerServiceTypes);
+
+        // 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);
+
+                // set the controller service type description
+                if (nf.Common.isDefinedAndNotNull(controllerServiceType)) {
+                    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();
+                }
+            }
+        });
+
+        // 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(args.current);
+        });
+        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();
+
+            // go through each controller service type
+            $.each(response.controllerServiceTypes, function (i, 
documentedType) {
+                // add the documented type
+                controllerServiceTypesData.addItem({
+                    id: id++,
+                    label: nf.Common.substringAfterLast(documentedType.type, 
'.'),
+                    type: documentedType.type,
+                    description: 
nf.Common.escapeHtml(documentedType.description),
+                    tags: documentedType.tags.join(', ')
+                });
+
+                // count the frequency of each tag for this type
+                $.each(documentedType.tags, function (i, tag) {
+                    tags.push(tag.toLowerCase());
+                });
+            });
+
+            // end the udpate
+            controllerServiceTypesData.endUpdate();
+
+            // set the total number of processors
+            $('#total-controller-service-types, 
#displayed-controller-service-types').text(response.controllerServiceTypes.length);
+
+            // 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,
+            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 controllerServiceTypesGrid = 
$('#controller-service-types-table').data('gridInstance');
+                    controllerServiceTypesGrid.setSelectedRows([]);
+                    controllerServiceTypesGrid.resetActiveCell();
+                }
+            }
+        });
+    };
+
+    /**
+     * Formatter for the name column.
+     *
+     * @param {type} row
+     * @param {type} cell
+     * @param {type} value
+     * @param {type} columnDef
+     * @param {type} dataContext
+     * @returns {String}
+     */
+    var nameFormatter = function (row, cell, value, columnDef, dataContext) {
+        if (!dataContext.accessPolicy.canRead) {
+            return '<span class="blank">' + dataContext.id + '</span>';
+        }
+        
+        return dataContext.component.name;
+    };
+
+    /**
+     * 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) {
+        if (!dataContext.accessPolicy.canRead) {
+            return '';
+        }
+        
+        return nf.Common.substringAfterLast(dataContext.component.type, '.');
+    };
+
+    /**
+     * 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) {
+            if (sortDetails.columnId === 'moreDetails') {
+                var aBulletins = 0;
+                if (!nf.Common.isEmpty(a.bulletins)) {
+                    aBulletins = a.bulletins.length;
+                }
+                var bBulletins = 0;
+                if (!nf.Common.isEmpty(b.bulletins)) {
+                    bBulletins = b.bulletins.length;
+                }
+                return aBulletins - bBulletins;
+            } else if (sortDetails.columnId === 'type') {
+                var aType = 
nf.Common.isDefinedAndNotNull(a[sortDetails.columnId]) ? 
nf.Common.substringAfterLast(a[sortDetails.columnId], '.') : '';
+                var bType = 
nf.Common.isDefinedAndNotNull(b[sortDetails.columnId]) ? 
nf.Common.substringAfterLast(b[sortDetails.columnId], '.') : '';
+                return aType === bType ? 0 : aType > bType ? 1 : -1;
+            } else if (sortDetails.columnId === 'state') {
+                var aState = 'Invalid';
+                if (nf.Common.isEmpty(a.validationErrors)) {
+                    aState = 
nf.Common.isDefinedAndNotNull(a[sortDetails.columnId]) ? 
a[sortDetails.columnId] : '';
+                }
+                var bState = 'Invalid';
+                if (nf.Common.isEmpty(b.validationErrors)) {
+                    bState = 
nf.Common.isDefinedAndNotNull(b[sortDetails.columnId]) ? 
b[sortDetails.columnId] : '';
+                }
+                return aState === bState ? 0 : aState > bState ? 1 : -1;
+            } else {
+                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.
+     * 
+     * @param {jQuery} serviceTable
+     */
+    var initControllerServices = function (serviceTable) {
+        // more details formatter
+        var moreControllerServiceDetails = function (row, cell, value, 
columnDef, dataContext) {
+            if (!dataContext.accessPolicy.canRead) {
+                return '';
+            }
+            
+            var markup = '<img src="images/iconDetails.png" title="View 
Details" class="pointer view-controller-service" style="margin-top: 5px; float: 
left;" />';
+
+            // always include a button to view the usage
+            markup += '<img src="images/iconUsage.png" title="Usage" 
class="pointer controller-service-usage" style="margin-left: 6px; margin-top: 
3px; float: left;" />';
+
+            var hasErrors = 
!nf.Common.isEmpty(dataContext.component.validationErrors);
+            var hasBulletins = 
!nf.Common.isEmpty(dataContext.component.bulletins);
+
+            if (hasErrors) {
+                markup += '<img src="images/iconAlert.png" class="has-errors" 
style="margin-top: 4px; margin-left: 3px; float: left;" />';
+            }
+
+            if (hasBulletins) {
+                markup += '<img src="images/iconBulletin.png" 
class="has-bulletins" style="margin-top: 5px; margin-left: 5px; float: 
left;"/>';
+            }
+
+            if (hasErrors || hasBulletins) {
+                markup += '<span class="hidden row-id">' + 
nf.Common.escapeHtml(dataContext.id) + '</span>';
+            }
+
+            return markup;
+        };
+
+        var controllerServiceStateFormatter = function (row, cell, value, 
columnDef, dataContext) {
+            if (!dataContext.accessPolicy.canRead) {
+                return '';
+            }
+            
+            // determine the appropriate label
+            var icon = '', label = '';
+            if (!nf.Common.isEmpty(dataContext.component.validationErrors)) {
+                icon = 'invalid';
+                label = 'Invalid';
+            } else {
+                if (dataContext.component.state === 'DISABLED') {
+                    icon = 'disabled';
+                    label = 'Disabled';
+                } else if (dataContext.component.state === 'DISABLING') {
+                    icon = 'disabled';
+                    label = 'Disabling';
+                } else if (dataContext.component.state === 'ENABLED') {
+                    icon = 'enabled';
+                    label = 'Enabled';
+                } else if (dataContext.component.state === '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 = '';
+
+            if (dataContext.accessPolicy.canRead && 
dataContext.accessPolicy.canWrite) {
+                if (dataContext.component.state === 'ENABLED' || 
dataContext.component.state === 'ENABLING') {
+                    markup += '<img src="images/iconDisable.png" 
title="Disable" class="pointer disable-controller-service" style="margin-top: 
2px;" />';
+                } else if (dataContext.component.state === 'DISABLED') {
+                    markup += '<img src="images/iconEdit.png" title="Edit" 
class="pointer edit-controller-service" style="margin-top: 2px;" />';
+
+                    // if there are no validation errors allow enabling
+                    if 
(nf.Common.isEmpty(dataContext.component.validationErrors)) {
+                        markup += '<img src="images/iconEnable.png" 
title="Enable" class="pointer enable-controller-service" style="margin-top: 
2px; margin-left: 3px;"/>';
+                    }
+
+                    markup += '<img src="images/iconDelete.png" title="Remove" 
class="pointer delete-controller-service" style="margin-top: 2px; margin-left: 
3px;" />';
+                }
+
+                if (dataContext.component.persistsState === true) {
+                    markup += '<img src="images/iconViewState.png" title="View 
State" class="pointer view-state-controller-service" style="margin-top: 2px; 
margin-left: 3px;" />';
+                }
+            }
+
+            return markup;
+        };
+
+        // define the column model for the controller services table
+        var controllerServicesColumns = [
+            {id: 'moreDetails', name: '&nbsp;', resizable: false, formatter: 
moreControllerServiceDetails, sortable: true, width: 90, maxWidth: 90, toolTip: 
'Sorts based on presence of bulletins'},
+            {id: 'name', name: 'Name', formatter: nameFormatter, sortable: 
true, resizable: true},
+            {id: 'type', name: 'Type', formatter: typeFormatter, sortable: 
true, resizable: true},
+            {id: 'state', name: 'State', formatter: 
controllerServiceStateFormatter, 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(serviceTable, 
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 controllerServiceEntity = 
controllerServicesData.getItem(args.row);
+
+            // determine the desired action
+            if (controllerServicesGrid.getColumns()[args.cell].id === 
'actions') {
+                if (target.hasClass('edit-controller-service')) {
+                    nf.ControllerService.showConfiguration(serviceTable, 
controllerServiceEntity);
+                } else if (target.hasClass('enable-controller-service')) {
+                    nf.ControllerService.enable(serviceTable, 
controllerServiceEntity);
+                } else if (target.hasClass('disable-controller-service')) {
+                    nf.ControllerService.disable(serviceTable, 
controllerServiceEntity);
+                } else if (target.hasClass('delete-controller-service')) {
+                    nf.ControllerService.remove(serviceTable, 
controllerServiceEntity);
+                } else if (target.hasClass('view-state-controller-service')) {
+                    
nf.ComponentState.showState(controllerServiceEntity.component, 
controllerServiceEntity.state === 'DISABLED');
+                }
+            } else if (controllerServicesGrid.getColumns()[args.cell].id === 
'moreDetails') {
+                if (target.hasClass('view-controller-service')) {
+                    nf.ControllerService.showDetails(serviceTable, 
controllerServiceEntity);
+                } else if (target.hasClass('controller-service-usage')) {
+                     // close the settings dialog
+                     $('#shell-close-button').click();
+
+                     // open the documentation for this controller service
+                     nf.Shell.showPage('../nifi-docs/documentation?' + 
$.param({
+                         select: 
nf.Common.substringAfterLast(controllerServiceEntity.component.type, '.')
+                     })).done(function() {
+                         nf.Settings.showSettings();
+                     });
+                 }
+            }
+        });
+
+        // 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
+        serviceTable.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 controllerServiceEntity = 
controllerServicesData.getItemById(serviceId);
+
+                // format the errors
+                var tooltip = 
nf.Common.formatUnorderedList(controllerServiceEntity.component.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));
+                }
+            }
+
+            var bulletinIcon = $(this).find('img.has-bulletins');
+            if (bulletinIcon.length && !bulletinIcon.data('qtip')) {
+                var taskId = $(this).find('span.row-id').text();
+
+                // get the task item
+                var controllerServiceEntity = 
controllerServicesData.getItemById(taskId);
+
+                // format the tooltip
+                var bulletins = 
nf.Common.getFormattedBulletins(controllerServiceEntity.component.bulletins);
+                var tooltip = nf.Common.formatUnorderedList(bulletins);
+
+                // show the tooltip
+                if (nf.Common.isDefinedAndNotNull(tooltip)) {
+                    bulletinIcon.qtip($.extend({}, 
nf.Common.config.tooltipConfig, {
+                        content: tooltip,
+                        position: {
+                            target: 'mouse',
+                            viewport: $(window),
+                            adjust: {
+                                x: 8,
+                                y: 8,
+                                method: 'flipinvert flipinvert'
+                            }
+                        }
+                    }));
+                }
+            }
+        });
+    };
+
+    /**
+     * Loads the controller services.
+     *
+     * @param {string} controllerServicesUri
+     * @param {jQuery} serviceTable
+     */
+    var loadControllerServices = function (controllerServicesUri, 
serviceTable) {
+        return $.ajax({
+            type: 'GET',
+            url: controllerServicesUri,
+            dataType: 'json'
+        }).done(function (response) {
+            var services = [];
+            $.each(response.controllerServices, function (_, service) {
+                services.push($.extend({
+                    bulletins: []
+                }, service));
+            });
+
+            nf.Common.cleanUpTooltips(serviceTable, 'img.has-errors');
+            nf.Common.cleanUpTooltips(serviceTable, 'img.has-bulletins');
+
+            var controllerServicesGrid = serviceTable.data('gridInstance');
+            var controllerServicesData = controllerServicesGrid.getData();
+
+            // update the controller services
+            controllerServicesData.setItems(services);
+            controllerServicesData.reSort();
+            controllerServicesGrid.invalidate();
+        });
+    };
+
+    return {
+        /**
+         * Initializes the status page.
+         * 
+         * @param {jQuery} serviceTable
+         */
+        init: function (serviceTable) {
+            if (!initialized) {
+                // initialize the new controller service dialog
+                initNewControllerServiceDialog();
+
+                // don't run this again
+                initialized = true;
+            }
+
+            // initialize the controller service table
+            initControllerServices(serviceTable);
+        },
+
+        /**
+         * Prompts for a new controller service.
+         *
+         * @param {string} controllerServicesUri
+         * @param {jQuery} serviceTable
+         */
+        promptNewControllerService: function (controllerServicesUri, 
serviceTable) {
+            // update the keyhandler
+            $('#controller-service-type-filter').off('keyup').on('keyup', 
function (e) {
+                var code = e.keyCode ? e.keyCode : e.which;
+                if (code === $.ui.keyCode.ENTER) {
+                    addSelectedControllerService(controllerServicesUri, 
serviceTable);
+                } else {
+                    applyControllerServiceTypeFilter();
+                }
+            });
+
+            // update the button model and show the dialog
+            $('#new-controller-service-dialog').modal('setButtonModel', [{
+                buttonText: 'Add',
+                handler: {
+                    click: function () {
+                        addSelectedControllerService(controllerServicesUri, 
serviceTable);
+                    }
+                }
+            }, {
+                buttonText: 'Cancel',
+                handler: {
+                    click: function () {
+                        $(this).modal('hide');
+                    }
+                }
+            }]).modal('show');
+
+            var controllerServiceTypesGrid = 
$('#controller-service-types-table').data('gridInstance');
+
+            // remove previous dbl click handler
+            if (dblClick !== null) {
+                controllerServiceTypesGrid.onDblClick.unsubscribe(dblClick);
+            }
+
+            // update the dbl click handler and subsrcibe
+            dblClick = function(e, args) {
+                var controllerServiceType = 
controllerServiceTypesGrid.getDataItem(args.row);
+                addControllerService(controllerServicesUri, serviceTable, 
controllerServiceType.type);
+            };
+            controllerServiceTypesGrid.onDblClick.subscribe(dblClick);
+
+            // reset the canvas size after the dialog is shown
+            if (nf.Common.isDefinedAndNotNull(controllerServiceTypesGrid)) {
+                controllerServiceTypesGrid.setSelectedRows([0]);
+                controllerServiceTypesGrid.resizeCanvas();
+            }
+
+            // set the initial focus
+            $('#controller-service-type-filter').focus();
+        },
+
+        /**
+         * Update the size of the grid based on its container's current size.
+         * 
+         * @param {jQuery} serviceTable
+         */
+        resetTableSize: function (serviceTable) {
+            var controllerServicesGrid = serviceTable.data('gridInstance');
+            if (nf.Common.isDefinedAndNotNull(controllerServicesGrid)) {
+                controllerServicesGrid.resizeCanvas();
+            }
+        },
+
+        /**
+         * Loads the settings.
+         *
+         * @param {string} controllerServicesUri
+         * @param {jQuery} serviceTable
+         */
+        loadControllerServices: function (controllerServicesUri, serviceTable) 
{
+            return loadControllerServices(controllerServicesUri, serviceTable);
+        },
+
+        /**
+         * Sets the controller service and reporting task bulletins in their 
respective tables.
+         *
+         * @param {jQuery} serviceTable
+         * @param {object} controllerServiceBulletins
+         */
+        setBulletins: function(serviceTable, controllerServiceBulletins) {
+            // controller services
+            var controllerServicesGrid = serviceTable.data('gridInstance');
+            var controllerServicesData = controllerServicesGrid.getData();
+            controllerServicesData.beginUpdate();
+
+            // if there are some bulletins process them
+            if (!nf.Common.isEmpty(controllerServiceBulletins)) {
+                var controllerServiceBulletinsBySource = d3.nest()
+                    .key(function(d) { return d.sourceId; })
+                    .map(controllerServiceBulletins, d3.map);
+
+                controllerServiceBulletinsBySource.forEach(function(sourceId, 
sourceBulletins) {
+                    var controllerService = 
controllerServicesData.getItemById(sourceId);
+                    if (nf.Common.isDefinedAndNotNull(controllerService)) {
+                        controllerServicesData.updateItem(sourceId, 
$.extend(controllerService, {
+                            bulletins: sourceBulletins
+                        }));
+                    }
+                });
+            } else {
+                // if there are no bulletins clear all
+                var controllerServices = controllerServicesData.getItems();
+                $.each(controllerServices, function(_, controllerService) {
+                    controllerServicesData.updateItem(controllerService.id, 
$.extend(controllerService, {
+                        bulletins: []
+                    }));
+                });
+            }
+            controllerServicesData.endUpdate();
+        }
+    };
+}());
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/9152a9fd/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
index d9d24b1..744869c 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
@@ -19,88 +19,271 @@
 
 nf.ProcessGroupConfiguration = (function () {
 
+    var config = {
+        filterText: 'Filter',
+        styles: {
+            filterList: 'filter-list'
+        },
+        urls: {
+            api: '../nifi-api'
+        }
+    };
+
+    /**
+     * Initializes the general tab.
+     */
+    var initGeneral = function () {
+    };
+
+    /**
+     * Gets the controller services table.
+     *
+     * @returns {*|jQuery|HTMLElement}
+     */
+    var getControllerServicesTable = function () {
+        return $('#process-group-controller-services-table');
+    };
+
+    /**
+     * Saves the configuration for the specified group.
+     *
+     * @param version
+     * @param groupId
+     */
+    var saveConfiguration = function (version, groupId) {
+        // build the entity
+        var entity = {
+            'revision': nf.Client.getRevision({
+                'revision': {
+                    'version': version
+                }
+            }),
+            'component': {
+                'id': groupId,
+                'name': $('#process-group-name').val(),
+                'comments': $('#process-group-comments').val()
+            }
+        };
+
+        // update the selected component
+        $.ajax({
+            type: 'PUT',
+            data: JSON.stringify(entity),
+            url: config.urls.api + '/process-groups/' + 
encodeURIComponent(groupId),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (response) {
+            // refresh the process group if necessary
+            if (response.accessPolicy.canRead && 
response.component.parentGroupId === nf.Canvas.getGroupId()) {
+                nf.ProcessGroup.set(response);
+            }
+
+            // show the result dialog
+            nf.Dialog.showOkDialog({
+                dialogContent: 'Process group configuration successfully 
saved.',
+                overlayBackground: false
+            });
+
+            // update the click listener for the updated revision
+            $('#process-group-configuration-save').off('click').on('click', 
function () {
+                saveConfiguration(response.revision.version, groupId);
+            });
+        }).fail(nf.Common.handleAjaxError);
+    };
+
+    /**
+     * Loads the configuration for the specified process group.
+     * 
+     * @param {string} groupId
+     */
+    var loadConfiguration = function (groupId) {
+        var setUnauthorizedText = function () {
+            
$('#read-only-process-group-name').addClass('unset').text('Unauthorized');
+            
$('#read-only-process-group-comments').addClass('unset').text('Unauthorized');
+        };
+
+        var setEditable = function (editable) {
+            if (editable) {
+                $('#process-group-configuration div.editable').show();
+                $('#process-group-configuration div.read-only').hide();
+                $('#process-group-configuration-save').show();
+            } else {
+                $('#process-group-configuration div.editable').hide();
+                $('#process-group-configuration div.read-only').show();
+                $('#process-group-configuration-save').hide();
+            }
+        };
+
+        var processGroup = $.Deferred(function (deferred) {
+            $.ajax({
+                type: 'GET',
+                url: config.urls.api + '/process-groups/' + 
encodeURIComponent(groupId),
+                dataType: 'json'
+            }).done(function (response) {
+                if (response.accessPolicy.canWrite) {
+                    var processGroup = response.component;
+
+                    // populate the process group settings
+                    $('#process-group-id').text(processGroup.id);
+                    
$('#process-group-name').removeClass('unset').val(processGroup.name);
+                    
$('#process-group-comments').removeClass('unset').val(processGroup.comments);
+
+                    setEditable(true);
+
+                    // register the click listener for the save button
+                    
$('#process-group-configuration-save').off('click').on('click', function () {
+                        saveConfiguration(response.revision.version, 
processGroupResponse.id);
+                    });
+                } else {
+                    if (response.accessPolicy.canRead) {
+                        // populate the process group settings
+                        
$('#read-only-process-group-name').removeClass('unset').text(response.component.name);
+                        
$('#read-only-process-group-comments').removeClass('unset').text(response.component.comments);
+                    } else {
+                        setUnauthorizedText();
+                    }
+
+                    setEditable(false);
+                }
+                deferred.resolve();
+            }).fail(function (xhr, status, error) {
+                if (xhr.status === 403) {
+                    setUnauthorizedText();
+                    setEditable(false);
+                    deferred.resolve();
+                } else {
+                    deferred.reject(xhr, status, error);
+                }
+            });
+        }).promise();
+
+        // load the controller services
+        var controllerServicesUri = config.urls.api + '/flow/process-groups/' 
+ encodeURIComponent(groupId) + '/controller-services';
+        var controllerServices = 
nf.ControllerServices.loadControllerServices(controllerServicesUri, 
getControllerServicesTable());
+        
+        // wait for everything to complete
+        return $.when(processGroup, 
controllerServices).fail(nf.Common.handleAjaxError);
+    };
+
+    /**
+     * Shows the process group configuration.
+     */
+    var showConfiguration = function () {
+        // show the configuration dialog
+        nf.Shell.showContent('#process-group-configuration').done(function () {
+            reset();
+        });
+
+        // adjust the table size
+        nf.ProcessGroupConfiguration.resetTableSize();
+    };
+
+    /**
+     * Resets the process group configuration dialog.
+     */
+    var reset = function () {
+        // reset button state
+        $('#process-group-configuration-save').mouseout();
+
+        // reset the fields
+        $('#process-group-name').val('');
+        $('#process-group-comments').val('');
+    };
+    
     return {
+        /**
+         * Initializes the settings page.
+         */
         init: function () {
-            $('#process-group-configuration').modal({
-                headerText: 'Configure Process Group',
-                overlayBackground: true,
-                buttons: [{
-                        buttonText: 'Apply',
-                        handler: {
-                            click: function () {
-                                // get the process group data to reference the 
uri
-                                var processGroupId = 
$('#process-group-id').text();
-                                var processGroupData = d3.select('#id-' + 
processGroupId).datum();
-                                
-                                // build the entity
-                                var entity = {
-                                    'revision': 
nf.Client.getRevision(processGroupData),
-                                    'component': {
-                                        'id': processGroupId,
-                                        'name': $('#process-group-name').val(),
-                                        'comments': 
$('#process-group-comments').val()
-                                    }
-                                };
-
-                                // update the selected component
-                                $.ajax({
-                                    type: 'PUT',
-                                    data: JSON.stringify(entity),
-                                    url: processGroupData.component.uri,
-                                    dataType: 'json',
-                                    contentType: 'application/json'
-                                }).done(function (response) {
-                                    // refresh the process group
-                                    nf.ProcessGroup.set(response);
-
-                                    // close the details panel
-                                    
$('#process-group-configuration').modal('hide');
-                                }).fail(function (xhr, status, error) {
-                                    // close the details panel
-                                    
$('#process-group-configuration').modal('hide');
-
-                                    // handle the error
-                                    nf.Common.handleAjaxError(xhr, status, 
error);
-                                });
-                            }
-                        }
-                    }, {
-                        buttonText: 'Cancel',
-                        handler: {
-                            click: function () {
-                                
$('#process-group-configuration').modal('hide');
+            // initialize the process group configuration tabs
+            $('#process-group-configuration-tabs').tabbs({
+                tabStyle: 'settings-tab',
+                selectedTabStyle: 'settings-selected-tab',
+                tabs: [{
+                    name: 'General',
+                    tabContentId: 
'general-process-group-configuration-tab-content'
+                }, {
+                    name: 'Controller Services',
+                    tabContentId: 
'process-group-controller-services-tab-content'
+                }],
+                select: function () {
+                    var tab = $(this).text();
+                    if (tab === 'General') {
+                        
$('#add-process-group-configuration-controller-service').hide();
+                    } else {
+                        
$('#add-process-group-configuration-controller-service').show();
+
+                        // update the tooltip on the button
+                        
$('#add-process-group-configuration-controller-service').attr('title', function 
() {
+                            if (tab === 'Controller Services') {
+                                return 'Create a new controller service';
                             }
-                        }
-                    }],
-                handler: {
-                    close: function () {
-                        // clear the process group details
-                        $('#process-group-id').text('');
-                        $('#process-group-name').val('');
-                        $('#process-group-comments').val('');
+                        });
+
+                        // resize the table
+                        nf.ProcessGroupConfiguration.resetTableSize();
                     }
                 }
             });
+
+            // settings refresh button...
+            
nf.Common.addHoverEffect('#process-group-configuration-refresh-button', 
'button-refresh', 'button-refresh-hover');
+
+            // handle window resizing
+            $(window).on('resize', function (e) {
+                nf.ProcessGroupConfiguration.resetTableSize();
+            });
+            
+            // initialize each tab
+            initGeneral();
+            nf.ControllerServices.init(getControllerServicesTable());
         },
         
         /**
-         * Shows the details for the specified selection.
-         * 
-         * @argument {selection} selection      The selection
+         * Update the size of the grid based on its container's current size.
          */
-        showConfiguration: function (selection) {
-            // if the specified selection is a processor, load its properties
-            if (nf.CanvasUtils.isProcessGroup(selection)) {
-                var selectionData = selection.datum();
-
-                // populate the process group settings
-                $('#process-group-id').text(selectionData.id);
-                $('#process-group-name').val(selectionData.component.name);
-                
$('#process-group-comments').val(selectionData.component.comments);
-
-                // show the details
-                $('#process-group-configuration').modal('show');
-            }
+        resetTableSize: function () {
+            nf.ControllerServices.resetTableSize(getControllerServicesTable());
+        },
+
+        /**
+         * Shows the settings dialog.
+         */
+        showConfiguration: function (groupId) {
+            // update the click listener
+            
$('#process-group-configuration-refresh-button').off('click').on('click', 
function () {
+                loadConfiguration(groupId).done(showConfiguration);
+            });
+
+            // update the new controller service click listener
+            
$('#add-process-group-configuration-controller-service').off('click').on('click',
 function () {
+                var selectedTab = $('#process-group-configuration-tabs 
li.settings-selected-tab').text();
+                if (selectedTab === 'Controller Services') {
+                    var controllerServicesUri = config.urls.api + 
'/process-groups/' + encodeURIComponent(groupId) + '/controller-services';
+                    
nf.ControllerServices.promptNewControllerService(controllerServicesUri, 
getControllerServicesTable());
+                }
+            });
+
+            // load the configuration
+            return loadConfiguration(groupId).done(showConfiguration);
+        },
+
+        /**
+         * Selects the specified controller service.
+         *
+         * @param {string} controllerServiceId
+         */
+        selectControllerService: function (controllerServiceId) {
+            var controllerServiceGrid = 
getControllerServicesTable().data('gridInstance');
+            var controllerServiceData = controllerServiceGrid.getData();
+
+            // select the desired service
+            var row = controllerServiceData.getRowById(controllerServiceId);
+            controllerServiceGrid.setSelectedRows([row]);
+            controllerServiceGrid.scrollRowIntoView(row);
+
+            // select the controller services tab
+            $('#process-group-configuration-tabs').find('li:eq(1)').click();
         }
     };
 }());
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/9152a9fd/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-reporting-task.js
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-reporting-task.js
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-reporting-task.js
index 9555163..ae31021 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-reporting-task.js
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-reporting-task.js
@@ -25,6 +25,15 @@ nf.ReportingTask = (function () {
     };
 
     /**
+     * Gets the controller services table.
+     *
+     * @returns {*|jQuery|HTMLElement}
+     */
+    var getControllerServicesTable = function () {
+        return $('#controller-services-table');
+    };
+    
+    /**
      * Handle any expected reporting task configuration errors.
      * 
      * @argument {object} xhr       The XmlHttpRequest
@@ -205,7 +214,7 @@ nf.ReportingTask = (function () {
         }).done(function (response) {
             // update the task
             renderReportingTask(response);
-            nf.ControllerService.reloadReferencedServices(response.component);
+            
nf.ControllerService.reloadReferencedServices(getControllerServicesTable(), 
response.component);
         }).fail(nf.Common.handleAjaxError);
     };
     
@@ -464,7 +473,7 @@ nf.ReportingTask = (function () {
                                 // save the reporting task
                                 
saveReportingTask(reportingTaskEntity).done(function (response) {
                                     // reload the reporting task
-                                    
nf.ControllerService.reloadReferencedServices(response.component);
+                                    
nf.ControllerService.reloadReferencedServices(getControllerServicesTable(), 
response.component);
 
                                     // close the details panel
                                     
$('#reporting-task-configuration').modal('hide');
@@ -497,7 +506,7 @@ nf.ReportingTask = (function () {
                                     
nf.CustomUi.showCustomUi($('#reporting-task-id').text(), 
reportingTask.customUiUrl, true).done(function () {
                                         // once the custom ui is closed, 
reload the reporting task
                                         
nf.ReportingTask.reload(reportingTaskEntity.id).done(function (response) {
-                                            
nf.ControllerService.reloadReferencedServices(response.reportingTask);
+                                            
nf.ControllerService.reloadReferencedServices(getControllerServicesTable(), 
response.reportingTask);
                                         });
                                         
                                         // show the settings

Reply via email to