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 2017/09/14 15:13:04 UTC

[1/4] nifi git commit: NIFI-4280: - Adding support for the user to configure variables in the UI. - Updating the endpoints for changing variables as necessary. This closes #2135.

Repository: nifi
Updated Branches:
  refs/heads/master 91383264d -> eac47e90c


http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-variable-registry.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-variable-registry.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-variable-registry.js
new file mode 100644
index 0000000..47b77ac
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-variable-registry.js
@@ -0,0 +1,1633 @@
+/*
+ * 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 define, module, require, exports */
+
+/**
+ * Opens the variable registry for a given Process Group.
+ */
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define(['jquery',
+                'd3',
+                'Slick',
+                'nf.Canvas',
+                'nf.CanvasUtils',
+                'nf.ErrorHandler',
+                'nf.Dialog',
+                'nf.Client',
+                'nf.Common',
+                'nf.ng.Bridge',
+                'nf.Processor',
+                'nf.ProcessGroup',
+                'nf.ProcessGroupConfiguration'],
+            function ($, d3, Slick, nfCanvas, nfCanvasUtils, nfErrorHandler, nfDialog, nfClient, nfCommon, nfNgBridge, nfProcessor, nfProcessGroup, nfProcessGroupConfiguration) {
+                return (nf.ComponentState = factory($, d3, Slick, nfCanvas, nfCanvasUtils, nfErrorHandler, nfDialog, nfClient, nfCommon, nfNgBridge, nfProcessor, nfProcessGroup, nfProcessGroupConfiguration));
+            });
+    } else if (typeof exports === 'object' && typeof module === 'object') {
+        module.exports = (nf.ComponentState =
+            factory(require('jquery'),
+                require('d3'),
+                require('Slick'),
+                require('nf.Canvas'),
+                require('nf.CanvasUtils'),
+                require('nf.ErrorHandler'),
+                require('nf.Dialog'),
+                require('nf.Client'),
+                require('nf.Common'),
+                require('nf.ng.Bridge'),
+                require('nf.Processor'),
+                require('nf.ProcessGroup'),
+                require('nf.ProcessGroupConfiguration')));
+    } else {
+        nf.VariableRegistry = factory(root.$,
+            root.d3,
+            root.Slick,
+            root.nf.Canvas,
+            root.nf.CanvasUtils,
+            root.nf.ErrorHandler,
+            root.nf.Dialog,
+            root.nf.Client,
+            root.nf.Common,
+            root.nf.ng.Bridge,
+            root.nf.Processor,
+            root.nf.ProcessGroup,
+            root.nf.ProcessGroupConfiguration);
+    }
+}(this, function ($, d3, Slick, nfCanvas, nfCanvasUtils, nfErrorHandler, nfDialog, nfClient, nfCommon, nfNgBridge, nfProcessor, nfProcessGroup, nfProcessGroupConfiguration) {
+    'use strict';
+
+    // text editor
+    var textEditor = function (args) {
+        var scope = this;
+        var initialValue = '';
+        var previousValue;
+        var wrapper;
+        var isEmpty;
+        var input;
+
+        this.init = function () {
+            var container = $('body');
+
+            // record the previous value
+            previousValue = args.item[args.column.field];
+
+            // create the wrapper
+            wrapper = $('<div></div>').addClass('slickgrid-editor').css({
+                'z-index': 100000,
+                'position': 'absolute',
+                'border-radius': '2px',
+                'box-shadow': 'rgba(0, 0, 0, 0.247059) 0px 2px 5px',
+                'background-color': 'rgb(255, 255, 255)',
+                'overflow': 'hidden',
+                'padding': '10px 20px',
+                'cursor': 'move',
+                'transform': 'translate3d(0px, 0px, 0px)'
+            }).appendTo(container);
+
+            // create the input field
+            input = $('<textarea hidefocus rows="5"/>').css({
+                'height': '80px',
+                'width': args.position.width + 'px',
+                'min-width': '212px',
+                'margin-bottom': '5px',
+                'margin-top': '10px',
+                'white-space': 'pre'
+            }).tab().on('keydown', scope.handleKeyDown).appendTo(wrapper);
+
+            wrapper.draggable({
+                cancel: '.button, textarea, .nf-checkbox',
+                containment: 'parent'
+            });
+
+            // create the button panel
+            var stringCheckPanel = $('<div class="string-check-container">');
+            stringCheckPanel.appendTo(wrapper);
+
+            // build the custom checkbox
+            isEmpty = $('<div class="nf-checkbox string-check"/>').appendTo(stringCheckPanel);
+            $('<span class="string-check-label nf-checkbox-label">&nbsp;Set empty string</span>').appendTo(stringCheckPanel);
+
+            var ok = $('<div class="button">Ok</div>').css({
+                'color': '#fff',
+                'background': '#728E9B'
+            }).hover(
+                function () {
+                    $(this).css('background', '#004849');
+                }, function () {
+                    $(this).css('background', '#728E9B');
+                }).on('click', scope.save);
+            var cancel = $('<div class="secondary-button">Cancel</div>').css({
+                'color': '#004849',
+                'background': '#E3E8EB'
+            }).hover(
+                function () {
+                    $(this).css('background', '#C7D2D7');
+                }, function () {
+                    $(this).css('background', '#E3E8EB');
+                }).on('click', scope.cancel);
+            $('<div></div>').css({
+                'position': 'relative',
+                'top': '10px',
+                'left': '20px',
+                'width': '212px',
+                'clear': 'both',
+                'float': 'right'
+            }).append(ok).append(cancel).append('<div class="clear"></div>').appendTo(wrapper);
+
+            // position and focus
+            scope.position(args.position);
+            input.focus().select();
+        };
+
+        this.handleKeyDown = function (e) {
+            if (e.which === $.ui.keyCode.ENTER && !e.shiftKey) {
+                scope.save();
+            } else if (e.which === $.ui.keyCode.ESCAPE) {
+                scope.cancel();
+
+                // prevent further propagation or escape press and prevent default behavior
+                e.stopImmediatePropagation();
+                e.preventDefault();
+            }
+        };
+
+        this.save = function () {
+            args.commitChanges();
+        };
+
+        this.cancel = function () {
+            input.val(initialValue);
+            args.cancelChanges();
+        };
+
+        this.hide = function () {
+            wrapper.hide();
+        };
+
+        this.show = function () {
+            wrapper.show();
+        };
+
+        this.position = function (position) {
+            wrapper.css({
+                'top': position.top - 27,
+                'left': position.left - 20
+            });
+        };
+
+        this.destroy = function () {
+            wrapper.remove();
+        };
+
+        this.focus = function () {
+            input.focus();
+        };
+
+        this.loadValue = function (item) {
+            var isEmptyChecked = false;
+
+            // determine the value to use when populating the text field
+            if (nfCommon.isDefinedAndNotNull(item[args.column.field])) {
+                initialValue = item[args.column.field];
+                isEmptyChecked = initialValue === '';
+            }
+
+            // determine if its an empty string
+            var checkboxStyle = isEmptyChecked ? 'checkbox-checked' : 'checkbox-unchecked';
+            isEmpty.addClass(checkboxStyle);
+
+            input.val(initialValue);
+            input.select();
+        };
+
+        this.serializeValue = function () {
+            // if the field has been cleared, set the value accordingly
+            if (input.val() === '') {
+                // if the user has checked the empty string checkbox, use emtpy string
+                if (isEmpty.hasClass('checkbox-checked')) {
+                    return '';
+                } else {
+                    return null;
+                }
+            } else {
+                // if there is text specified, use that value
+                return input.val();
+            }
+        };
+
+        this.applyValue = function (item, state) {
+            item[args.column.field] = state;
+        };
+
+        this.isValueChanged = function () {
+            return scope.serializeValue() !== previousValue;
+        };
+
+        this.validate = function () {
+            return {
+                valid: true,
+                msg: null
+            };
+        };
+
+        // initialize the custom long text editor
+        this.init();
+    };
+
+    /**
+     * Shows the variable in a read only property detail winder.
+     *
+     * @param {object} variable
+     * @param {slickgrid} variableGrid
+     * @param {integer} row
+     * @param {integer} cell
+     */
+    var showVariableValue = function (variable, variableGrid, row, cell) {
+        var cellNode = $(variableGrid.getCellNode(row, cell));
+        var offset = cellNode.offset();
+
+        var wrapper = $('<div class="property-detail"></div>').css({
+            'z-index': 1999,
+            'position': 'absolute',
+            'padding': '10px 20px',
+            'overflow': 'hidden',
+            'border-radius': '2px',
+            'box-shadow': 'rgba(0, 0, 0, 0.247059) 0px 2px 5px',
+            'background-color': 'rgb(255, 255, 255)',
+            'cursor': 'move',
+            'transform': 'translate3d(0px, 0px, 0px)',
+            'top': offset.top - 26,
+            'left': offset.left - 20
+        }).draggable({
+            containment: 'parent'
+        }).appendTo('body');
+
+        // create the input field
+        $('<textarea hidefocus rows="5" readonly="readonly"/>').css({
+            'height': '80px',
+            'resize': 'both',
+            'width': cellNode.width() + 'px',
+            'margin': '10px 0px',
+            'white-space': 'pre'
+        }).text(variable.value).on('keydown', function (evt) {
+            if (evt.which === $.ui.keyCode.ESCAPE) {
+                cleanUp();
+
+                evt.stopImmediatePropagation();
+                evt.preventDefault();
+            }
+        }).appendTo(wrapper);
+
+        var cleanUp = function () {
+            wrapper.hide().remove();
+        };
+
+        // add an ok button that will remove the entire pop up
+        var ok = $('<div class="button">Ok</div>').css({
+            'position': 'relative',
+            'top': '10px',
+            'left': '20px'
+        }).hover(
+            function () {
+                $(this).css('background', '#004849');
+            }, function () {
+                $(this).css('background', '#728E9B');
+            }).on('click', function () {
+            cleanUp();
+        });
+
+        $('<div></div>').append(ok).append('<div class="clear"></div>').appendTo(wrapper);
+    };
+
+    var gridOptions = {
+        forceFitColumns: true,
+        enableTextSelectionOnCells: true,
+        enableCellNavigation: true,
+        enableColumnReorder: false,
+        editable: true,
+        enableAddRow: false,
+        autoEdit: false,
+        multiSelect: false,
+        rowHeight: 24
+    };
+
+    /**
+     * Gets the scope label for the specified Process Group.
+     *
+     * @param {string} processGroupId
+     * @returns {string} the label for the specified Process Group
+     */
+    var getScopeLabel = function (processGroupId) {
+        // see if this listing is based off a selected process group
+        var selection = nfCanvasUtils.getSelection();
+        if (selection.empty() === false) {
+            var selectedData = selection.datum();
+            if (selectedData.id === processGroupId) {
+                if (selectedData.permissions.canRead) {
+                    return nfCommon.escapeHtml(selectedData.component.name);
+                } else {
+                    return nfCommon.escapeHtml(selectedData.id);
+                }
+            }
+        }
+
+        // there's either no selection or the variable is defined in an ancestor component
+        var breadcrumbs = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+
+        var processGroupLabel = processGroupId;
+        $.each(breadcrumbs, function (_, breadcrumbEntity) {
+            if (breadcrumbEntity.id === processGroupId) {
+                processGroupLabel = breadcrumbEntity.label;
+                return false;
+            }
+        });
+
+        return processGroupLabel;
+    };
+
+    /**
+     * Initializes the variable table
+     */
+    var initVariableTable = function () {
+        var variableTable = $('#variable-registry-table');
+
+        var nameFormatter = function (row, cell, value, columnDef, dataContext) {
+            return nfCommon.escapeHtml(value);
+        };
+
+        var valueFormatter = function (row, cell, value, columnDef, dataContext) {
+            if (dataContext.isOverridden) {
+                return '<div class="overridden" title="This value has been overridden by another variable in a descendant Process Group">' + nfCommon.escapeHtml(value) + '</div>';
+            } else {
+                if (value === '') {
+                    return '<span class="table-cell blank">Empty string set</span>';
+                } else if (value === null) {
+                    return '<span class="unset">No value set</span>';
+                } else {
+                    return nfCommon.escapeHtml(value);
+                }
+            }
+        };
+
+        var scopeFormatter = function (row, cell, value, columnDef, dataContext) {
+            if (nfCommon.isDefinedAndNotNull(value)) {
+                return nfCommon.escapeHtml(getScopeLabel(value));
+            } else {
+                return 'Controller';
+            }
+        };
+
+        var variableActionFormatter = function (row, cell, value, columnDef, dataContext) {
+            var markup = '';
+
+            if (dataContext.isEditable === true) {
+                markup += '<div title="Delete" class="delete-variable pointer fa fa-trash" style="margin-top: 2px;" ></div>';
+            } else {
+                var currentProcessGroupId = $('#variable-registry-process-group-id').text();
+
+                if (dataContext.processGroupId !== currentProcessGroupId) {
+                    markup += '<div title="Go To" class="go-to-variable pointer fa fa-long-arrow-right" style="margin-top: 2px;" ></div>';
+                }
+            }
+
+            return markup;
+        };
+
+        // define the column model for the controller services table
+        var variableColumns = [
+            {
+                id: 'scope',
+                name: 'Scope',
+                field: 'processGroupId',
+                formatter: scopeFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'name',
+                name: 'Name',
+                field: 'name',
+                formatter: nameFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'value',
+                name: 'Value',
+                field: 'value',
+                formatter: valueFormatter,
+                sortable: true,
+                resizable: true,
+                cssClass: 'pointer'
+            },
+            {
+                id: 'actions',
+                name: '&nbsp;',
+                resizable: false,
+                formatter: variableActionFormatter,
+                sortable: false,
+                width: 45,
+                maxWidth: 45
+            }
+        ];
+
+        // initialize the dataview
+        var variableData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+        variableData.setFilterArgs({
+            searchString: '',
+            property: 'hidden'
+        });
+        variableData.setFilter(function (item, args) {
+            return item.hidden === false;
+        });
+        variableData.getItemMetadata = function (index) {
+            return {
+                columns: {
+                    value: {
+                        editor: textEditor
+                    }
+                }
+            };
+        };
+
+        // initialize the sort
+        sortVariables({
+            columnId: 'name',
+            sortAsc: true
+        }, variableData);
+
+        // initialize the grid
+        var variablesGrid = new Slick.Grid(variableTable, variableData, variableColumns, gridOptions);
+        variablesGrid.setSelectionModel(new Slick.RowSelectionModel());
+        variablesGrid.registerPlugin(new Slick.AutoTooltips());
+        variablesGrid.setSortColumn('name', true);
+        variablesGrid.onSort.subscribe(function (e, args) {
+            sortVariables({
+                columnId: args.sortCol.id,
+                sortAsc: args.sortAsc
+            }, variableData);
+        });
+        variablesGrid.onClick.subscribe(function (e, args) {
+            // get the variable at this row
+            var variable = variableData.getItem(args.row);
+
+            if (variablesGrid.getColumns()[args.cell].id === 'value') {
+                if (variable.isEditable === true) {
+                    variablesGrid.gotoCell(args.row, args.cell, true);
+                } else {
+                    // ensure the row is selected
+                    variablesGrid.setSelectedRows([args.row]);
+
+                    // show the variable
+                    showVariableValue(variable, variablesGrid, args.row, args.cell);
+                }
+
+                // prevents standard edit logic
+                e.stopImmediatePropagation();
+            } else if (variablesGrid.getColumns()[args.cell].id === 'actions') {
+                var target = $(e.target);
+
+                // determine the desired action
+                if (target.hasClass('delete-variable')) {
+                    // mark the property in question for removal and refresh the table
+                    variableData.updateItem(variable.id, $.extend(variable, {
+                        hidden: true
+                    }));
+
+                    // look if this variable that was just 'removed'
+                    var variables = variableData.getItems();
+                    $.each(variables, function (_, item) {
+                        if (item.isOverridden === true && !isOverridden(variables, item)) {
+                            variableData.updateItem(item.id, $.extend(item, {
+                                isOverridden: false
+                            }));
+                        }
+                    });
+
+                    // reset the selection if necessary
+                    var selectedRows = variablesGrid.getSelectedRows();
+                    if (selectedRows.length === 0) {
+                        variablesGrid.setSelectedRows([0]);
+                    }
+
+                    // prevents standard edit logic
+                    e.stopImmediatePropagation();
+                } else if (target.hasClass('go-to-variable')) {
+                    // check if there are outstanding changes
+                    handleOutstandingChanges().done(function () {
+                        // go to the process group that this variable belongs to
+                        var breadcrumbs = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+                        $.each(breadcrumbs, function (_, breadcrumbEntity) {
+                            // find the breadcrumb for the process group of the variable
+                            if (breadcrumbEntity.id === variable.processGroupId) {
+                                // if that breadcrumb has a parent breadcrumb, navigate to the parent group and select the PG
+                                if (nfCommon.isDefinedAndNotNull(breadcrumbEntity.parentBreadcrumb)) {
+                                    nfCanvasUtils.showComponent(breadcrumbEntity.parentBreadcrumb.id, breadcrumbEntity.id).done(function () {
+                                        setTimeout(function () {
+                                            // open the variable dialog for the process group of this variable
+                                            showVariables(variable.processGroupId, variable.name);
+                                        }, 500);
+                                    });
+                                } else {
+                                    nfCanvasUtils.getComponentByType('ProcessGroup').enterGroup(breadcrumbEntity.id).done(function () {
+                                        setTimeout(function () {
+                                            // open the variable dialog for the process group of this variable
+                                            showVariables(variable.processGroupId, variable.name);
+                                        }, 500);
+                                    });
+                                }
+
+                                return false;
+                            }
+                        });
+                    });
+                }
+            }
+        });
+        variablesGrid.onSelectedRowsChanged.subscribe(function (e, args) {
+            if ($.isArray(args.rows) && args.rows.length === 1) {
+                // show the affected components for the selected variable
+                if (variablesGrid.getDataLength() > 0) {
+                    var variableIndex = args.rows[0];
+                    var variable = variablesGrid.getDataItem(variableIndex);
+
+                    // update the details for this variable
+                    $('#affected-components-context').removeClass('unset').text(variable.name);
+                    populateAffectedComponents(variable.affectedComponents);
+                }
+            }
+        });
+
+        // wire up the dataview to the grid
+        variableData.onRowCountChanged.subscribe(function (e, args) {
+            variablesGrid.updateRowCount();
+            variablesGrid.render();
+        });
+        variableData.onRowsChanged.subscribe(function (e, args) {
+            variablesGrid.invalidateRows(args.rows);
+            variablesGrid.render();
+        });
+        variableData.syncGridSelection(variablesGrid, true);
+
+        // hold onto an instance of the grid
+        variableTable.data('gridInstance', variablesGrid);
+    };
+
+    /**
+     * Handles outstanding changes.
+     *
+     * @returns {deferred}
+     */
+    var handleOutstandingChanges = function () {
+        var variableGrid = $('#variable-registry-table').data('gridInstance');
+        if (nfCommon.isDefinedAndNotNull(variableGrid)) {
+            // get the property grid to commit the current edit
+            var editController = variableGrid.getEditController();
+            editController.commitCurrentEdit();
+        }
+
+        return $.Deferred(function (deferred) {
+            if ($('#variable-update-status').is(':visible')) {
+                close();
+                deferred.resolve();
+            } else {
+                var variables = marshalVariables();
+
+                // if there are no variables there is nothing to save
+                if ($.isEmptyObject(variables)) {
+                    close();
+                    deferred.resolve();
+                } else {
+                    // see if those changes should be saved
+                    nfDialog.showYesNoDialog({
+                        headerText: 'Variables',
+                        dialogContent: 'Save changes before leaving variable configuration?',
+                        noHandler: function () {
+                            close();
+                            deferred.resolve();
+                        },
+                        yesHandler: function () {
+                            updateVariables().done(function () {
+                                deferred.resolve();
+                            }).fail(function () {
+                                deferred.reject();
+                            });
+                        }
+                    });
+                }
+            }
+
+        }).promise();
+    };
+
+    /**
+     * Sorts the specified data using the specified sort details.
+     *
+     * @param {object} sortDetails
+     * @param {object} data
+     */
+    var sortVariables = function (sortDetails, data) {
+        // defines a function for sorting
+        var comparer = function (a, b) {
+            if (sortDetails.columnId === 'scope') {
+                var aScope = nfCommon.isDefinedAndNotNull(a.processGroupId) ? getScopeLabel(a.processGroupId) : '';
+                var bScope = nfCommon.isDefinedAndNotNull(b.processGroupId) ? getScopeLabel(b.processGroupId) : '';
+                return aScope === bScope ? 0 : aScope > bScope ? 1 : -1;
+            } else {
+                var aString = nfCommon.isDefinedAndNotNull(a[sortDetails.columnId]) ? a[sortDetails.columnId] : '';
+                var bString = nfCommon.isDefinedAndNotNull(b[sortDetails.columnId]) ? b[sortDetails.columnId] : '';
+                return aString === bString ? 0 : aString > bString ? 1 : -1;
+            }
+        };
+
+        // perform the sort
+        data.sort(comparer, sortDetails.sortAsc);
+    };
+
+    /**
+     * Sorts the specified entities based on the name.
+     *
+     * @param {object} a
+     * @param {pbject} b
+     * @returns {number}
+     */
+    var nameComparator = function (a, b) {
+        return a.component.name.localeCompare(b.component.name);
+    };
+
+    /**
+     * Renders the specified affected component.
+     *
+     * @param {object} affectedProcessorEntity
+     * @param {jQuery} container
+     */
+    var renderAffectedProcessor = function (affectedProcessorEntity, container) {
+        var affectedProcessorContainer = $('<li class="affected-component-container"></li>').appendTo(container);
+        var affectedProcessor = affectedProcessorEntity.component;
+
+        // processor state
+        $('<div class="referencing-component-state"></div>').addClass(function () {
+            if (nfCommon.isDefinedAndNotNull(affectedProcessor.state)) {
+                var icon = $(this);
+
+                var state = affectedProcessor.state.toLowerCase();
+                if (state === 'stopped' && !nfCommon.isEmpty(affectedProcessor.validationErrors)) {
+                    state = 'invalid';
+
+                    // build the validation error listing
+                    var list = nfCommon.formatUnorderedList(affectedProcessor.validationErrors);
+
+                    // add tooltip for the warnings
+                    icon.qtip($.extend({},
+                        nfCanvasUtils.config.systemTooltipConfig,
+                        {
+                            content: list
+                        }));
+                }
+
+                return state;
+            } else {
+                return '';
+            }
+        }).appendTo(affectedProcessorContainer);
+
+
+        // processor name
+        $('<span class="referencing-component-name link"></span>').text(affectedProcessor.name).on('click', function () {
+            // check if there are outstanding changes
+            handleOutstandingChanges().done(function () {
+                // show the component in question
+                nfCanvasUtils.showComponent(affectedProcessor.processGroupId, affectedProcessor.id);
+            });
+        }).appendTo(affectedProcessorContainer);
+
+        // bulletin
+        $('<div class="referencing-component-bulletins"></div>').addClass(affectedProcessor.id + '-affected-bulletins').appendTo(affectedProcessorContainer);
+
+        // processor active threads
+        $('<span class="referencing-component-active-thread-count"></span>').text(function () {
+            if (nfCommon.isDefinedAndNotNull(affectedProcessor.activeThreadCount) && affectedProcessor.activeThreadCount > 0) {
+                return '(' + affectedProcessor.activeThreadCount + ')';
+            } else {
+                return '';
+            }
+        }).appendTo(affectedProcessorContainer);
+    };
+
+    /**
+     * Renders the specified affect controller service.
+     *
+     * @param {object} affectedControllerServiceEntity
+     * @param {jQuery} container
+     */
+    var renderAffectedControllerService = function (affectedControllerServiceEntity, container) {
+        var affectedControllerServiceContainer = $('<li class="affected-component-container"></li>').appendTo(container);
+        var affectedControllerService = affectedControllerServiceEntity.component;
+
+        // controller service state
+        $('<div class="referencing-component-state"></div>').addClass(function () {
+            if (nfCommon.isDefinedAndNotNull(affectedControllerService.state)) {
+                var icon = $(this);
+
+                var state = affectedControllerService.state === 'ENABLED' ? 'enabled' : 'disabled';
+                if (state === 'disabled' && !nfCommon.isEmpty(affectedControllerService.validationErrors)) {
+                    state = 'invalid';
+
+                    // build the error listing
+                    var list = nfCommon.formatUnorderedList(affectedControllerService.validationErrors);
+
+                    // add tooltip for the warnings
+                    icon.qtip($.extend({},
+                        nfCanvasUtils.config.systemTooltipConfig,
+                        {
+                            content: list
+                        }));
+                }
+                return state;
+            } else {
+                return '';
+            }
+        }).appendTo(affectedControllerServiceContainer);
+
+        // bulletin
+        $('<div class="referencing-component-bulletins"></div>').addClass(affectedControllerService.id + '-affected-bulletins').appendTo(affectedControllerServiceContainer);
+
+        // controller service name
+        $('<span class="link"></span>').text(affectedControllerService.name).on('click', function () {
+            // check if there are outstanding changes
+            handleOutstandingChanges().done(function () {
+                // show the component in question
+                nfProcessGroupConfiguration.showConfiguration(affectedControllerService.processGroupId).done(function () {
+                    nfProcessGroupConfiguration.selectControllerService(affectedControllerService.id);
+                });
+            });
+        }).appendTo(affectedControllerServiceContainer);
+    };
+
+    /**
+     * Populates the affected components for the specified variable.
+     *
+     * @param {object} affectedComponents
+     */
+    var populateAffectedComponents = function (affectedComponents) {
+        var affectedProcessors = [];
+        var affectedControllerServices = [];
+        var unauthorizedAffectedComponents = [];
+
+        // clear the affected components from the previous selection
+        var processorContainer = $('#variable-registry-affected-processors');
+        nfCommon.cleanUpTooltips(processorContainer, 'div.referencing-component-state');
+        nfCommon.cleanUpTooltips(processorContainer, 'div.referencing-component-bulletins');
+        processorContainer.empty();
+
+        var controllerServiceContainer = $('#variable-registry-affected-controller-services');
+        nfCommon.cleanUpTooltips(controllerServiceContainer, 'div.referencing-component-state');
+        nfCommon.cleanUpTooltips(controllerServiceContainer, 'div.referencing-component-bulletins');
+        controllerServiceContainer.empty();
+
+        var unauthorizedComponentsContainer = $('#variable-registry-affected-unauthorized-components').empty();
+
+        // affected component will be undefined when a new variable is added
+        if (nfCommon.isUndefined(affectedComponents)) {
+            $('<li class="affected-component-container"><span class="unset">Pending Apply</span></li>').appendTo(processorContainer);
+            $('<li class="affected-component-container"><span class="unset">Pending Apply</span></li>').appendTo(controllerServiceContainer);
+            $('<li class="affected-component-container"><span class="unset">Pending Apply</span></li>').appendTo(unauthorizedComponentsContainer);
+        } else {
+            var referencingComponentsForBulletinRetrieval = [];
+
+            // bin the affected components according to their type
+            $.each(affectedComponents, function (_, affectedComponentEntity) {
+                if (affectedComponentEntity.permissions.canRead === true && affectedComponentEntity.permissions.canWrite === true) {
+                    referencingComponentsForBulletinRetrieval.push(affectedComponentEntity.id);
+
+                    if (affectedComponentEntity.component.referenceType === 'PROCESSOR') {
+                        affectedProcessors.push(affectedComponentEntity);
+                    } else {
+                        affectedControllerServices.push(affectedComponentEntity);
+                    }
+                } else {
+                    // if we're unauthorized only because the user is lacking write permissions, we can still query for bulletins
+                    if (affectedComponentEntity.permissions.canRead === true) {
+                        referencingComponentsForBulletinRetrieval.push(affectedComponentEntity.id);
+                    }
+
+                    unauthorizedAffectedComponents.push(affectedComponentEntity);
+                }
+            });
+
+            if (affectedProcessors.length === 0) {
+                $('<li class="affected-component-container"><span class="unset">None</span></li>').appendTo(processorContainer);
+            } else {
+                // sort the affected processors
+                affectedProcessors.sort(nameComparator);
+
+                // render each and register a click handler
+                $.each(affectedProcessors, function (_, affectedProcessorEntity) {
+                    renderAffectedProcessor(affectedProcessorEntity, processorContainer);
+                });
+            }
+
+            if (affectedControllerServices.length === 0) {
+                $('<li class="affected-component-container"><span class="unset">None</span></li>').appendTo(controllerServiceContainer);
+            } else {
+                // sort the affected controller services
+                affectedControllerServices.sort(nameComparator);
+
+                // render each and register a click handler
+                $.each(affectedControllerServices, function (_, affectedControllerServiceEntity) {
+                    renderAffectedControllerService(affectedControllerServiceEntity, controllerServiceContainer);
+                });
+            }
+
+            if (unauthorizedAffectedComponents.length === 0) {
+                $('<li class="affected-component-container"><span class="unset">None</span></li>').appendTo(unauthorizedComponentsContainer);
+            } else {
+                // sort the unauthorized affected components
+                unauthorizedAffectedComponents.sort(function (a, b) {
+                    if (a.permissions.canRead === true && b.permissions.canRead === true) {
+                        // processors before controller services
+                        var sortVal = a.component.referenceType === b.component.referenceType ? 0 : a.component.referenceType > b.component.referenceType ? -1 : 1;
+
+                        // if a and b are the same type, then sort by name
+                        if (sortVal === 0) {
+                            sortVal = a.component.name === b.component.name ? 0 : a.component.name > b.component.name ? 1 : -1;
+                        }
+
+                        return sortVal;
+                    } else {
+
+                        // if lacking read and write perms on both, sort by id
+                        if (a.permissions.canRead === false && b.permissions.canRead === false) {
+                            return a.id > b.id ? 1 : -1;
+                        } else {
+                            // if only one has read perms, then let it come first
+                            if (a.permissions.canRead === true) {
+                                return -1;
+                            } else {
+                                return 1;
+                            }
+                        }
+                    }
+                });
+
+                $.each(unauthorizedAffectedComponents, function (_, unauthorizedAffectedComponentEntity) {
+                    if (unauthorizedAffectedComponentEntity.permissions.canRead === true) {
+                        if (unauthorizedAffectedComponentEntity.component.referenceType === 'PROCESSOR') {
+                            renderAffectedProcessor(unauthorizedAffectedComponentEntity, unauthorizedComponentsContainer);
+                        } else {
+                            renderAffectedControllerService(unauthorizedAffectedComponentEntity, unauthorizedComponentsContainer);
+                        }
+                    } else {
+                        var affectedUnauthorizedComponentContainer = $('<li class="affected-component-container"></li>').appendTo(unauthorizedComponentsContainer);
+                        $('<span class="unset"></span>').text(unauthorizedAffectedComponentEntity.id).appendTo(affectedUnauthorizedComponentContainer);
+                    }
+                });
+            }
+
+            // query for the bulletins
+            if (referencingComponentsForBulletinRetrieval.length > 0) {
+                nfCanvasUtils.queryBulletins(referencingComponentsForBulletinRetrieval).done(function (response) {
+                    var bulletins = response.bulletinBoard.bulletins;
+
+                    var bulletinsBySource = d3.nest()
+                        .key(function (d) {
+                            return d.sourceId;
+                        })
+                        .map(bulletins, d3.map);
+
+                    bulletinsBySource.forEach(function (sourceId, sourceBulletins) {
+                        $('div.' + sourceId + '-affected-bulletins').each(function () {
+                            var bulletinIcon = $(this);
+
+                            // if there are bulletins update them
+                            if (sourceBulletins.length > 0) {
+                                // format the new bulletins
+                                var formattedBulletins = nfCommon.getFormattedBulletins(sourceBulletins);
+
+                                var list = nfCommon.formatUnorderedList(formattedBulletins);
+
+                                // update existing tooltip or initialize a new one if appropriate
+                                bulletinIcon.addClass('has-bulletins').show().qtip($.extend({},
+                                    nfCanvasUtils.config.systemTooltipConfig,
+                                    {
+                                        content: list
+                                    }));
+                            }
+                        });
+                    });
+                });
+            }
+        }
+    };
+
+    /**
+     * Shows the variable for the specified processGroupId.
+     *
+     * @param {string} processGroupId
+     * @param {string} variableToSelect to select
+     */
+    var showVariables = function (processGroupId, variableToSelect) {
+        return $.ajax({
+            type: 'GET',
+            url: '../nifi-api/process-groups/' + encodeURIComponent(processGroupId) + '/variable-registry',
+            dataType: 'json'
+        }).done(function (response) {
+            $('#process-group-variable-registry').text(getScopeLabel(processGroupId));
+            $('#variable-registry-process-group-id').text(processGroupId).data('revision', response.processGroupRevision);
+
+            // load the variables
+            loadVariables(response.variableRegistry, variableToSelect);
+
+            // show the dialog
+            $('#variable-registry-dialog').modal('show');
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Returns whether the currentVariable is overridden in the specified variables.
+     *
+     * @param {array} variables
+     * @param {object} currentVariable
+     * @returns {boolean} whether currentVariable is overridden
+     */
+    var isOverridden = function(variables, currentVariable) {
+        // identify any variables conflicting with the current variable
+        var conflictingVariables = [];
+        $.each(variables, function (_, variable) {
+            if (currentVariable.name === variable.name && variable.hidden === false) {
+                conflictingVariables.push(variable);
+            }
+        });
+
+        var isOverridden = false;
+
+        // if there are any variables conflicting
+        if (conflictingVariables.length > 1) {
+            var ancestry = [];
+
+            // get the breadcrumbs to walk the ancestry
+            var breadcrumbs = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+            $.each(breadcrumbs, function (_, breadcrumbEntity) {
+                ancestry.push(breadcrumbEntity.id);
+            });
+
+            // check to see if the current process group is not part of the ancestry
+            var currentProcessGroupId = $('#variable-registry-process-group-id').text();
+            if (ancestry.indexOf(currentProcessGroupId) === -1) {
+                ancestry.push(currentProcessGroupId);
+            }
+
+            // go through each group in the ancestry
+            $.each(ancestry, function (_, processGroupId) {
+                // for each breadcrumb go through each variable
+                for (var i = 0; i < conflictingVariables.length; i++) {
+
+                    // if this breadcrumb represents the process group for the conflicting variable
+                    if (processGroupId === conflictingVariables[i].processGroupId) {
+
+                        // if this conflicting variable is the current variable, mark as overridden as we
+                        // know there is at least one more conflicting variable
+                        if (currentVariable === conflictingVariables[i]) {
+                            isOverridden = true;
+                        }
+
+                        conflictingVariables.splice(i, 1);
+
+                        // if we are left with only a single variable break out of the breadcrumb iteration
+                        if (conflictingVariables.length === 1) {
+                            return false;
+                        }
+                    }
+                }
+            });
+        }
+
+        return isOverridden;
+    };
+
+    /**
+     * Returns whether the specified variable is editable.
+     *
+     * @param {object} variable
+     * @returns {boolean} if variable is editable
+     */
+    var isEditable = function (variable) {
+        // if the variable can be written based on the perms of the affected components
+        if (variable.canWrite === true) {
+            var currentProcessGroupId = $('#variable-registry-process-group-id').text();
+
+            // only support configuration if the variable belongs to the current group
+            if (variable.processGroupId === currentProcessGroupId) {
+
+                // verify the permissions of the group
+                var selection = nfCanvasUtils.getSelection();
+                if (selection.empty() === false && nf.CanvasUtils.isProcessGroup(selection)) {
+                    var selectedData = selection.datum();
+                    if (selectedData.id === currentProcessGroupId) {
+                        return selectedData.permissions.canWrite === true;
+                    }
+                }
+
+                var canWrite = false;
+                var breadcrumbs = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+                $.each(breadcrumbs, function (_, breadcrumbEntity) {
+                    if (breadcrumbEntity.id === currentProcessGroupId) {
+                        canWrite = breadcrumbEntity.permissions.canWrite === true;
+                        return false;
+                    }
+                });
+
+                return canWrite;
+            }
+        }
+
+        return false;
+    };
+
+    /**
+     * Loads the specified variable registry.
+     *
+     * @param {object} variableRegistry
+     * @param {string} variableToSelect to select
+     */
+    var loadVariables = function (variableRegistry, variableToSelect) {
+        if (nfCommon.isDefinedAndNotNull(variableRegistry)) {
+            var count = 0;
+            var index = 0;
+
+            var variableGrid = $('#variable-registry-table').data('gridInstance');
+            var variableData = variableGrid.getData();
+
+            // begin the update
+            variableData.beginUpdate();
+
+            var variables = [];
+            $.each(variableRegistry.variables, function (i, variableEntity) {
+                var variable = variableEntity.variable;
+                variables.push({
+                    id: count++,
+                    hidden: false,
+                    canWrite: variableEntity.canWrite,
+                    name: variable.name,
+                    value: variable.value,
+                    previousValue: variable.value,
+                    processGroupId: variable.processGroupId,
+                    affectedComponents: variable.affectedComponents
+                });
+            });
+
+            $.each(variables, function (i, variable) {
+                variableData.addItem($.extend({
+                    isOverridden: isOverridden(variables, variable),
+                    isEditable: isEditable(variable)
+                }, variable));
+            });
+
+            // complete the update
+            variableData.endUpdate();
+            variableData.reSort();
+
+            // if we are pre-selecting a specific variable, get it's index
+            if (nfCommon.isDefinedAndNotNull(variableToSelect)) {
+                $.each(variables, function (i, variable) {
+                    if (variableRegistry.processGroupId === variable.processGroupId && variable.name === variableToSelect) {
+                        index = variableData.getRowById(variable.id);
+                    }
+                });
+            }
+
+            if (variables.length === 0) {
+                // empty the containers
+                var processorContainer = $('#variable-registry-affected-processors');
+                nfCommon.cleanUpTooltips(processorContainer, 'div.referencing-component-state');
+                nfCommon.cleanUpTooltips(processorContainer, 'div.referencing-component-bulletins');
+                processorContainer.empty();
+
+                var controllerServiceContainer = $('#variable-registry-affected-controller-services');
+                nfCommon.cleanUpTooltips(controllerServiceContainer, 'div.referencing-component-state');
+                nfCommon.cleanUpTooltips(controllerServiceContainer, 'div.referencing-component-bulletins');
+                controllerServiceContainer.empty();
+
+                var unauthorizedComponentsContainer = $('#variable-registry-affected-unauthorized-components').empty();
+
+                // indicate no affected components
+                $('<li class="affected-component-container"><span class="unset">None</span></li>').appendTo(processorContainer);
+                $('<li class="affected-component-container"><span class="unset">None</span></li>').appendTo(controllerServiceContainer);
+                $('<li class="affected-component-container"><span class="unset">None</span></li>').appendTo(unauthorizedComponentsContainer);
+
+                // update the selection context
+                $('#affected-components-context').addClass('unset').text('None');
+            } else {
+                // select the desired row
+                variableGrid.setSelectedRows([index]);
+            }
+        }
+    };
+
+    /**
+     * Populates the variable update steps.
+     *
+     * @param {array} updateSteps
+     * @param {boolean} whether this request has been cancelled
+     * @param {boolean} whether this request has errored
+     */
+    var populateVariableUpdateStep = function (updateSteps, cancelled, errored) {
+        var updateStatusContainer = $('#variable-update-steps').empty();
+
+        // go through each step
+        $.each(updateSteps, function (_, updateStep) {
+            var stepItem = $('<li></li>').text(updateStep.description).appendTo(updateStatusContainer);
+
+            $('<div class="variable-step"></div>').addClass(function () {
+                 if (nfCommon.isDefinedAndNotNull(updateStep.failureReason)) {
+                     return 'ajax-error';
+                 } else {
+                     if (updateStep.complete === true) {
+                         return 'ajax-complete';
+                     } else {
+                         return cancelled === true || errored === true ? 'ajax-error' : 'ajax-loading';
+                     }
+                 }
+            }).appendTo(stepItem);
+
+            $('<div class="clear"></div>').appendTo(stepItem);
+        });
+    };
+
+    /**
+     * Updates variables by issuing an update request and polling until it's completion.
+     */
+    var updateVariables = function () {
+        var variables = marshalVariables();
+        if (variables.length === 0) {
+            close();
+            return;
+        }
+
+        // update the variables context
+        var variableNames = variables.map(function (v) {
+             return v.variable.name;
+        });
+        $('#affected-components-context').removeClass('unset').text(variableNames.join(', '));
+
+        // get the current group id
+        var processGroupId = $('#variable-registry-process-group-id').text();
+
+        return $.Deferred(function (deferred) {
+            // updates the button model to show the close button
+            var updateToCloseButtonModel = function () {
+                $('#variable-registry-dialog').modal('setButtonModel', [{
+                    buttonText: 'Close',
+                    color: {
+                        base: '#728E9B',
+                        hover: '#004849',
+                        text: '#ffffff'
+                    },
+                    handler: {
+                        click: function () {
+                            deferred.resolve();
+                            close();
+                        }
+                    }
+                }]);
+            };
+
+            var cancelled = false;
+
+            // update the button model to show the cancel button
+            $('#variable-registry-dialog').modal('setButtonModel', [{
+                buttonText: 'Cancel',
+                color: {
+                    base: '#E3E8EB',
+                    hover: '#C7D2D7',
+                    text: '#004849'
+                },
+                handler: {
+                    click: function () {
+                        cancelled = true;
+                        updateToCloseButtonModel()
+                    }
+                }
+            }]);
+
+            var requestId;
+            var handleAjaxFailure = function (xhr, status, error) {
+                // delete the request if possible
+                if (nfCommon.isDefinedAndNotNull(requestId)) {
+                    deleteUpdateRequest(processGroupId, requestId);
+                }
+
+                // update the step status
+                $('#variable-update-steps').find('div.variable-step.ajax-loading').removeClass('ajax-loading').addClass('ajax-error');
+
+                // update the button model
+                updateToCloseButtonModel();
+            };
+
+            submitUpdateRequest(processGroupId, variables).done(function (response) {
+                var pollUpdateRequest = function (updateRequestEntity) {
+                    var updateRequest = updateRequestEntity.request;
+                    var errored = nfCommon.isDefinedAndNotNull(updateRequest.failureReason);
+
+                    // get the request id
+                    requestId = updateRequest.requestId;
+
+                    // update the affected components
+                    populateAffectedComponents(updateRequest.affectedComponents);
+
+                    // update the progress/steps
+                    populateVariableUpdateStep(updateRequest.updateSteps, cancelled, errored);
+
+                    // if this request was cancelled, remove the update request
+                    if (cancelled) {
+                        deleteUpdateRequest(updateRequest.processGroupId, requestId);
+                    } else {
+                        if (updateRequest.complete === true) {
+                            if (errored) {
+                                nfDialog.showOkDialog({
+                                    headerText: 'Variable Update Error',
+                                    dialogContent: 'Unable to complete variable update request: ' + nfCommon.escapeHtml(updateRequest.failureReason)
+                                });
+                            }
+
+                            // reload affected processors
+                            $.each(updateRequest.affectedComponents, function (_, affectedComponentEntity) {
+                                if (affectedComponentEntity.permissions.canRead === true) {
+                                    var affectedComponent = affectedComponentEntity.component;
+
+                                    // reload the process if it's in the current group
+                                    if (affectedComponent.referenceType === 'PROCESSOR' && nfCanvasUtils.getGroupId() === affectedComponent.processGroupId) {
+                                        nfProcessor.reload(affectedComponent.id);
+                                    }
+                                }
+                            });
+
+                            // reload the process group if the context of the update is not the current group (meaning its a child of the current group)
+                            if (nfCanvasUtils.getGroupId() !== updateRequest.processGroupId) {
+                                nfProcessGroup.reload(updateRequest.processGroupId);
+                            }
+
+                            // delete the update request
+                            deleteUpdateRequest(updateRequest.processGroupId, requestId);
+
+                            // update the button model
+                            updateToCloseButtonModel();
+                        } else {
+                            // wait to get an updated status
+                            setTimeout(function () {
+                                getUpdateRequest(updateRequest.processGroupId, requestId).done(function (getResponse) {
+                                    pollUpdateRequest(getResponse);
+                                }).fail(handleAjaxFailure);
+                            }, 2000);
+                        }
+                    }
+                };
+
+                // update the visibility
+                $('#variable-registry-table, #add-variable').hide();
+                $('#variable-update-status').show();
+
+                pollUpdateRequest(response);
+            }).fail(handleAjaxFailure);
+        }).promise();
+    };
+
+    /**
+     * Submits an variable update request.
+     *
+     * @param {string} processGroupId
+     * @param {object} variables
+     * @returns {deferred} update request xhr
+     */
+    var submitUpdateRequest = function (processGroupId, variables) {
+        var processGroupRevision = $('#variable-registry-process-group-id').data('revision');
+
+        var updateRequestEntity = {
+            processGroupRevision: nfClient.getRevision({
+                revision: {
+                    version: processGroupRevision.version
+                }
+            }),
+            variableRegistry: {
+                processGroupId: processGroupId,
+                variables: variables
+            }
+        };
+
+        return $.ajax({
+            type: 'POST',
+            data: JSON.stringify(updateRequestEntity),
+            url: '../nifi-api/process-groups/' + encodeURIComponent(processGroupId) + '/variable-registry/update-requests',
+            dataType: 'json',
+            contentType: 'application/json'
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Obtains the current state of the updateRequest using the specified process group id and update request id.
+     *
+     * @param {string} processGroupId
+     * @param {string} updateRequestId
+     * @returns {deferred} update request xhr
+     */
+    var getUpdateRequest = function (processGroupId, updateRequestId) {
+        return $.ajax({
+            type: 'GET',
+            url: '../nifi-api/process-groups/' + encodeURIComponent(processGroupId) + '/variable-registry/update-requests/' + encodeURIComponent(updateRequestId),
+            dataType: 'json'
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Deletes an updateRequest using the specified process group id and update request id.
+     *
+     * @param {string} processGroupId
+     * @param {string} updateRequestId
+     * @returns {deferred} update request xhr
+     */
+    var deleteUpdateRequest = function (processGroupId, updateRequestId) {
+        return $.ajax({
+            type: 'DELETE',
+            url: '../nifi-api/process-groups/' + encodeURIComponent(processGroupId) + '/variable-registry/update-requests/' + encodeURIComponent(updateRequestId),
+            dataType: 'json'
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Marshals the variables in the table.
+     */
+    var marshalVariables = function () {
+        var variables = [];
+
+        var variableGrid = $('#variable-registry-table').data('gridInstance');
+        var variableData = variableGrid.getData();
+
+        $.each(variableData.getItems(), function () {
+            var variable = {
+                'name': this.name
+            };
+
+            var modified = false;
+            if (this.hidden === true && this.previousValue !== null) {
+                // hidden variables were removed by the user, clear the value
+                variable['value'] = null;
+                modified = true;
+            } else if (this.value !== this.previousValue) {
+                // the value has changed
+                variable['value'] = this.value;
+                modified = true;
+            }
+
+            if (modified) {
+                variables.push({
+                    'variable': variable
+                });
+            }
+        });
+
+        return variables;
+    };
+
+    /**
+     * Adds a new variable.
+     */
+    var addNewVariable = function () {
+        var currentProcessGroupId = $('#variable-registry-process-group-id').text();
+        var variableName = $.trim($('#new-variable-name').val());
+
+        // ensure the property name is specified
+        if (variableName !== '') {
+            var variableGrid = $('#variable-registry-table').data('gridInstance');
+            var variableData = variableGrid.getData();
+
+            // ensure the property name is unique
+            var conflictingVariables = [];
+            var matchingVariable = null;
+            $.each(variableData.getItems(), function (_, item) {
+                if (variableName === item.name) {
+                    // if the scope is same, this is an exact match otherwise we've identified a conflicting variable
+                    if (currentProcessGroupId === item.processGroupId) {
+                        matchingVariable = item;
+                    } else {
+                        conflictingVariables.push(item);
+                    }
+                }
+            });
+
+            if (matchingVariable === null) {
+                // add a row for the new variable
+                var id = variableData.getLength();
+                variableData.addItem({
+                    id: id,
+                    hidden: false,
+                    canWrite: true,
+                    name: variableName,
+                    value: null,
+                    previousValue: null,
+                    processGroupId: currentProcessGroupId,
+                    isEditable: true,
+                    isOverridden: false
+                });
+
+                // we've just added a new variable, mark any conflicting variables as overridden
+                $.each(conflictingVariables, function (_, conflictingVariable) {
+                    variableData.updateItem(conflictingVariable.id, $.extend(conflictingVariable, {
+                        isOverridden: true
+                    }));
+                });
+
+                // sort the data
+                variableData.reSort();
+
+                // select the new variable row
+                var row = variableData.getRowById(id);
+                variableGrid.setActiveCell(row, variableGrid.getColumnIndex('value'));
+                variableGrid.editActiveCell();
+            } else {
+                // if this row is currently hidden, clear the value and show it
+                if (matchingVariable.hidden === true) {
+                    variableData.updateItem(matchingVariable.id, $.extend(matchingVariable, {
+                        hidden: false,
+                        value: null
+                    }));
+
+                    // select the new properties row
+                    var editableMatchingRow = variableData.getRowById(matchingVariable.id);
+                    variableGrid.setActiveCell(editableMatchingRow, variableGrid.getColumnIndex('value'));
+                    variableGrid.editActiveCell();
+                } else {
+                    nfDialog.showOkDialog({
+                        headerText: 'Variable Exists',
+                        dialogContent: 'A variable with this name already exists.'
+                    });
+
+                    // select the existing properties row
+                    var matchingRow = variableData.getRowById(matchingVariable.id);
+                    variableGrid.setSelectedRows([matchingRow]);
+                    variableGrid.scrollRowIntoView(matchingRow);
+                }
+            }
+        } else {
+            nfDialog.showOkDialog({
+                headerText: 'Variable Name',
+                dialogContent: 'Variable name must be specified.'
+            });
+        }
+
+        // close the new variable dialog
+        $('#new-variable-dialog').modal('hide');
+    };
+
+    /**
+     * Cancels adding a new variable.
+     */
+    var close = function () {
+        $('#variable-registry-dialog').modal('hide');
+    };
+
+    /**
+     * Reset the dialog.
+     */
+    var resetDialog = function () {
+        $('#variable-registry-table, #add-variable').show();
+        $('#variable-update-status').hide();
+
+        $('#process-group-variable-registry').text('');
+        $('#variable-registry-process-group-id').text('').removeData('revision');
+        $('#affected-components-context').removeClass('unset').text('');
+
+        var variableGrid = $('#variable-registry-table').data('gridInstance');
+        var variableData = variableGrid.getData();
+        variableData.setItems([]);
+
+        var affectedProcessorContainer = $('#variable-registry-affected-processors');
+        nfCommon.cleanUpTooltips(affectedProcessorContainer, 'div.referencing-component-state');
+        nfCommon.cleanUpTooltips(affectedProcessorContainer, 'div.referencing-component-bulletins');
+        affectedProcessorContainer.empty();
+
+        var affectedControllerServicesContainer = $('#variable-registry-affected-controller-services');
+        nfCommon.cleanUpTooltips(affectedControllerServicesContainer, 'div.referencing-component-state');
+        nfCommon.cleanUpTooltips(affectedControllerServicesContainer, 'div.referencing-component-bulletins');
+        affectedControllerServicesContainer.empty();
+
+        $('#variable-registry-affected-unauthorized-components').empty();
+    };
+
+    return {
+        /**
+         * Initializes the variable registry dialogs.
+         */
+        init: function () {
+            $('#variable-registry-dialog').modal({
+                scrollableContentStyle: 'scrollable',
+                headerText: 'Variables',
+                handler: {
+                    close: function () {
+                        resetDialog();
+                    },
+                    open: function () {
+                        var variableGrid = $('#variable-registry-table').data('gridInstance');
+                        if (nfCommon.isDefinedAndNotNull(variableGrid)) {
+                            variableGrid.resizeCanvas();
+                        }
+                    }
+                }
+            });
+
+            $('#new-variable-dialog').modal({
+                headerText: 'New Variable',
+                buttons: [{
+                    buttonText: 'Ok',
+                    color: {
+                        base: '#728E9B',
+                        hover: '#004849',
+                        text: '#ffffff'
+                    },
+                    handler: {
+                        click: function () {
+                            addNewVariable();
+                        }
+                    }
+                }],
+                handler: {
+                    close: function () {
+                        $('#new-variable-name').val('');
+                    },
+                    open: function () {
+                        $('#new-variable-name').focus();
+                    }
+                }
+            });
+
+            $('#new-variable-name').on('keydown', function (e) {
+                var code = e.keyCode ? e.keyCode : e.which;
+                if (code === $.ui.keyCode.ENTER) {
+                    addNewVariable();
+
+                    // prevents the enter from propagating into the field for editing the new property value
+                    e.stopImmediatePropagation();
+                    e.preventDefault();
+                }
+            });
+
+            $('#add-variable').on('click', function () {
+                $('#new-variable-dialog').modal('show');
+            });
+
+            initVariableTable();
+        },
+
+        /**
+         * Shows the variables for the specified process group.
+         *
+         * @param {string} processGroupId
+         */
+        showVariables: function (processGroupId) {
+            // restore the button model
+            $('#variable-registry-dialog').modal('setButtonModel', [{
+                buttonText: 'Apply',
+                color: {
+                    base: '#728E9B',
+                    hover: '#004849',
+                    text: '#ffffff'
+                },
+                handler: {
+                    click: function () {
+                        updateVariables();
+                    }
+                }
+            }, {
+                buttonText: 'Cancel',
+                color: {
+                    base: '#E3E8EB',
+                    hover: '#C7D2D7',
+                    text: '#004849'
+                },
+                handler: {
+                    click: function () {
+                        close();
+                    }
+                }
+            }]);
+
+            return showVariables(processGroupId);
+        }
+    };
+}));
\ No newline at end of file


[3/4] nifi git commit: NIFI-4280: - Adding support for the user to configure variables in the UI. - Updating the endpoints for changing variables as necessary. This closes #2135.

Posted by ma...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index 0b634a1..0e574e0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -16,56 +16,18 @@
  */
 package org.apache.nifi.web.api;
 
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBElement;
-import javax.xml.bind.JAXBException;
-import javax.xml.bind.Unmarshaller;
-import javax.xml.transform.stream.StreamSource;
-
+import com.sun.jersey.api.core.ResourceContext;
+import com.sun.jersey.core.util.MultivaluedMapImpl;
+import com.sun.jersey.multipart.FormDataParam;
+import com.wordnik.swagger.annotations.Api;
+import com.wordnik.swagger.annotations.ApiOperation;
+import com.wordnik.swagger.annotations.ApiParam;
+import com.wordnik.swagger.annotations.ApiResponse;
+import com.wordnik.swagger.annotations.ApiResponses;
+import com.wordnik.swagger.annotations.Authorization;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.AuthorizeAccess;
 import org.apache.nifi.authorization.AuthorizeControllerServiceReference;
 import org.apache.nifi.authorization.Authorizer;
 import org.apache.nifi.authorization.ComponentAuthorizable;
@@ -75,19 +37,18 @@ import org.apache.nifi.authorization.SnippetAuthorizable;
 import org.apache.nifi.authorization.TemplateContentsAuthorizable;
 import org.apache.nifi.authorization.resource.Authorizable;
 import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserDetails;
 import org.apache.nifi.authorization.user.NiFiUserUtils;
 import org.apache.nifi.bundle.BundleCoordinate;
+import org.apache.nifi.cluster.manager.NodeResponse;
 import org.apache.nifi.connectable.ConnectableType;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.serialization.FlowEncodingVersion;
 import org.apache.nifi.controller.service.ControllerServiceState;
-import org.apache.nifi.framework.security.util.SslContextFactory;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateRequest;
 import org.apache.nifi.registry.variable.VariableRegistryUpdateStep;
 import org.apache.nifi.remote.util.SiteToSiteRestApiClient;
 import org.apache.nifi.util.BundleUtils;
-import org.apache.nifi.util.FormatUtils;
-import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.NiFiServiceFacade;
 import org.apache.nifi.web.ResourceNotFoundException;
 import org.apache.nifi.web.Revision;
@@ -106,9 +67,9 @@ import org.apache.nifi.web.api.dto.RevisionDTO;
 import org.apache.nifi.web.api.dto.TemplateDTO;
 import org.apache.nifi.web.api.dto.VariableRegistryDTO;
 import org.apache.nifi.web.api.dto.flow.FlowDTO;
-import org.apache.nifi.web.api.dto.status.ProcessGroupStatusDTO;
-import org.apache.nifi.web.api.dto.status.ProcessGroupStatusSnapshotDTO;
+import org.apache.nifi.web.api.dto.status.ProcessorStatusDTO;
 import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.ConnectionEntity;
 import org.apache.nifi.web.api.entity.ConnectionsEntity;
 import org.apache.nifi.web.api.entity.ControllerServiceEntity;
@@ -125,8 +86,6 @@ import org.apache.nifi.web.api.entity.LabelsEntity;
 import org.apache.nifi.web.api.entity.OutputPortsEntity;
 import org.apache.nifi.web.api.entity.PortEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupEntity;
-import org.apache.nifi.web.api.entity.ProcessGroupStatusEntity;
-import org.apache.nifi.web.api.entity.ProcessGroupStatusSnapshotEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupsEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
 import org.apache.nifi.web.api.entity.ProcessorsEntity;
@@ -138,23 +97,61 @@ import org.apache.nifi.web.api.entity.VariableRegistryEntity;
 import org.apache.nifi.web.api.entity.VariableRegistryUpdateRequestEntity;
 import org.apache.nifi.web.api.request.ClientIdParameter;
 import org.apache.nifi.web.api.request.LongParameter;
+import org.apache.nifi.web.security.token.NiFiAuthenticationToken;
 import org.apache.nifi.web.util.Pause;
-import org.apache.nifi.web.util.WebUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
 
-import com.sun.jersey.api.client.Client;
-import com.sun.jersey.api.client.ClientResponse;
-import com.sun.jersey.api.client.config.ClientConfig;
-import com.sun.jersey.api.client.config.DefaultClientConfig;
-import com.sun.jersey.api.core.ResourceContext;
-import com.sun.jersey.multipart.FormDataParam;
-import com.wordnik.swagger.annotations.Api;
-import com.wordnik.swagger.annotations.ApiOperation;
-import com.wordnik.swagger.annotations.ApiParam;
-import com.wordnik.swagger.annotations.ApiResponse;
-import com.wordnik.swagger.annotations.ApiResponses;
-import com.wordnik.swagger.annotations.Authorization;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.transform.stream.StreamSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * RESTful endpoint for managing a Group.
@@ -455,14 +452,12 @@ public class ProcessGroupResource extends ApplicationResource {
             throw new IllegalArgumentException("Group ID and Update ID must both be specified.");
         }
 
-        if (isReplicateRequest()) {
-            return replicate(HttpMethod.GET);
-        }
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
         // authorize access
         serviceFacade.authorizeAccess(lookup -> {
             final Authorizable processGroup = lookup.getProcessGroup(groupId).getAuthorizable();
-            processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
+            processGroup.authorize(authorizer, RequestAction.READ, user);
         });
 
         final VariableRegistryUpdateRequest request = varRegistryUpdateRequests.get(updateId);
@@ -474,9 +469,14 @@ public class ProcessGroupResource extends ApplicationResource {
             throw new ResourceNotFoundException("Could not find a Variable Registry Update Request with identifier " + updateId + " for Process Group with identifier " + groupId);
         }
 
+        if (!user.equals(request.getUser())) {
+            throw new IllegalArgumentException("Only the user that submitted the update request can retrieve it.");
+        }
+
         final VariableRegistryUpdateRequestEntity entity = new VariableRegistryUpdateRequestEntity();
-        entity.setRequestDto(dtoFactory.createVariableRegistryUpdateRequestDto(request));
-        entity.getRequestDto().setUri(generateResourceUri("process-groups", groupId, "variable-registry", updateId));
+        entity.setRequest(dtoFactory.createVariableRegistryUpdateRequestDto(request));
+        entity.setProcessGroupRevision(request.getProcessGroupRevision());
+        entity.getRequest().setUri(generateResourceUri("process-groups", groupId, "variable-registry", updateId));
         return generateOkResponse(entity).build();
     }
 
@@ -506,15 +506,13 @@ public class ProcessGroupResource extends ApplicationResource {
             throw new IllegalArgumentException("Group ID and Update ID must both be specified.");
         }
 
-        if (isReplicateRequest()) {
-            return replicate(HttpMethod.DELETE);
-        }
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
 
         // authorize access
         serviceFacade.authorizeAccess(lookup -> {
             final Authorizable processGroup = lookup.getProcessGroup(groupId).getAuthorizable();
-            processGroup.authorize(authorizer, RequestAction.READ, NiFiUserUtils.getNiFiUser());
-            processGroup.authorize(authorizer, RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
+            processGroup.authorize(authorizer, RequestAction.READ, user);
+            processGroup.authorize(authorizer, RequestAction.WRITE, user);
         });
 
         final VariableRegistryUpdateRequest request = varRegistryUpdateRequests.remove(updateId);
@@ -526,11 +524,16 @@ public class ProcessGroupResource extends ApplicationResource {
             throw new ResourceNotFoundException("Could not find a Variable Registry Update Request with identifier " + updateId + " for Process Group with identifier " + groupId);
         }
 
+        if (!user.equals(request.getUser())) {
+            throw new IllegalArgumentException("Only the user that submitted the update request can remove it.");
+        }
+
         request.cancel();
 
         final VariableRegistryUpdateRequestEntity entity = new VariableRegistryUpdateRequestEntity();
-        entity.setRequestDto(dtoFactory.createVariableRegistryUpdateRequestDto(request));
-        entity.getRequestDto().setUri(generateResourceUri("process-groups", groupId, "variable-registry", updateId));
+        entity.setRequest(dtoFactory.createVariableRegistryUpdateRequestDto(request));
+        entity.setProcessGroupRevision(request.getProcessGroupRevision());
+        entity.getRequest().setUri(generateResourceUri("process-groups", groupId, "variable-registry", updateId));
         return generateOkResponse(entity).build();
     }
 
@@ -634,11 +637,11 @@ public class ProcessGroupResource extends ApplicationResource {
         // 1. Determine Affected Components (this includes any Processors and Controller Services and any components that reference an affected Controller Service).
         //    1a. Determine ID's of components
         //    1b. Determine Revision's of associated components
-        // 2. Stop All Affected Processors
-        // 3. Disable All Affected Controller Services
+        // 2. Stop All Active Affected Processors
+        // 3. Disable All Active Affected Controller Services
         // 4. Update the Variables
-        // 5. Re-Enable all Affected Controller Services (services only, not dependent components)
-        // 6. Re-Enable all Processors that Depended on the Controller Services
+        // 5. Re-Enable all previously Active Affected Controller Services (services only, not dependent components)
+        // 6. Re-Enable all previously Active Processors that Depended on the Controller Services
 
         // Determine the affected components (and their associated revisions)
         final VariableRegistryEntity computedEntity = serviceFacade.populateAffectedComponents(requestEntity.getVariableRegistry());
@@ -647,38 +650,77 @@ public class ProcessGroupResource extends ApplicationResource {
             throw new ResourceNotFoundException(String.format("Unable to locate group with id '%s'.", groupId));
         }
 
-        final Set<AffectedComponentDTO> affectedComponents = serviceFacade.getComponentsAffectedByVariableRegistryUpdate(requestEntity.getVariableRegistry());
+        final Set<AffectedComponentEntity> allAffectedComponents = serviceFacade.getComponentsAffectedByVariableRegistryUpdate(requestEntity.getVariableRegistry());
+        final Set<AffectedComponentDTO> activeAffectedComponents = serviceFacade.getActiveComponentsAffectedByVariableRegistryUpdate(requestEntity.getVariableRegistry());
 
-        final Map<String, List<AffectedComponentDTO>> affectedComponentsByType = affectedComponents.stream()
-            .collect(Collectors.groupingBy(comp -> comp.getComponentType()));
+        final Map<String, List<AffectedComponentDTO>> activeAffectedComponentsByType = activeAffectedComponents.stream()
+            .collect(Collectors.groupingBy(comp -> comp.getReferenceType()));
 
-        final List<AffectedComponentDTO> affectedProcessors = affectedComponentsByType.get(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR);
-        final List<AffectedComponentDTO> affectedServices = affectedComponentsByType.get(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE);
+        final List<AffectedComponentDTO> activeAffectedProcessors = activeAffectedComponentsByType.get(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR);
+        final List<AffectedComponentDTO> activeAffectedServices = activeAffectedComponentsByType.get(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE);
 
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        // define access authorize for execution below
+        final AuthorizeAccess authorizeAccess = lookup -> {
+            final Authorizable groupAuthorizable = lookup.getProcessGroup(groupId).getAuthorizable();
+            groupAuthorizable.authorize(authorizer, RequestAction.WRITE, user);
+
+            // For every component that is affected, the user must have READ permissions and WRITE permissions
+            // (because this action requires stopping the component).
+            if (activeAffectedProcessors != null) {
+                for (final AffectedComponentDTO activeAffectedComponent : activeAffectedProcessors) {
+                    final Authorizable authorizable = lookup.getProcessor(activeAffectedComponent.getId()).getAuthorizable();
+                    authorizable.authorize(authorizer, RequestAction.READ, user);
+                    authorizable.authorize(authorizer, RequestAction.WRITE, user);
+                }
+            }
+
+            if (activeAffectedServices != null) {
+                for (final AffectedComponentDTO activeAffectedComponent : activeAffectedServices) {
+                    final Authorizable authorizable = lookup.getControllerService(activeAffectedComponent.getId()).getAuthorizable();
+                    authorizable.authorize(authorizer, RequestAction.READ, user);
+                    authorizable.authorize(authorizer, RequestAction.WRITE, user);
+                }
+            }
+        };
 
         if (isReplicateRequest()) {
+            // authorize access
+            serviceFacade.authorizeAccess(authorizeAccess);
+
             // update the variable registry
-            final VariableRegistryUpdateRequest updateRequest = createVariableRegistryUpdateRequest(groupId);
+            final VariableRegistryUpdateRequest updateRequest = createVariableRegistryUpdateRequest(groupId, allAffectedComponents, user);
             updateRequest.getIdentifyRelevantComponentsStep().setComplete(true);
             final URI originalUri = getAbsolutePath();
 
             // Submit the task to be run in the background
             final Runnable taskWrapper = () -> {
                 try {
-                    updateVariableRegistryReplicated(groupId, originalUri, affectedProcessors, affectedServices, updateRequest, requestEntity);
+                    // set the user authentication token
+                    final Authentication authentication = new NiFiAuthenticationToken(new NiFiUserDetails(user));
+                    SecurityContextHolder.getContext().setAuthentication(authentication);
+
+                    updateVariableRegistryReplicated(groupId, originalUri, activeAffectedProcessors, activeAffectedServices, updateRequest, requestEntity);
                 } catch (final Exception e) {
                     logger.error("Failed to update variable registry", e);
+
+                    updateRequest.setComplete(true);
                     updateRequest.setFailureReason("An unexpected error has occurred: " + e);
+                } finally {
+                    // clear the authentication token
+                    SecurityContextHolder.getContext().setAuthentication(null);
                 }
             };
 
             variableRegistryThreadPool.submit(taskWrapper);
 
             final VariableRegistryUpdateRequestEntity responseEntity = new VariableRegistryUpdateRequestEntity();
-            responseEntity.setRequestDto(dtoFactory.createVariableRegistryUpdateRequestDto(updateRequest));
-            responseEntity.getRequestDto().setUri(generateResourceUri("process-groups", groupId, "variable-registry", "update-requests", updateRequest.getRequestId()));
+            responseEntity.setRequest(dtoFactory.createVariableRegistryUpdateRequestDto(updateRequest));
+            responseEntity.setProcessGroupRevision(updateRequest.getProcessGroupRevision());
+            responseEntity.getRequest().setUri(generateResourceUri("process-groups", groupId, "variable-registry", "update-requests", updateRequest.getRequestId()));
 
-            final URI location = URI.create(responseEntity.getRequestDto().getUri());
+            final URI location = URI.create(responseEntity.getRequest().getUri());
             return Response.status(Status.ACCEPTED).location(location).entity(responseEntity).build();
         }
 
@@ -688,34 +730,10 @@ public class ProcessGroupResource extends ApplicationResource {
             serviceFacade,
             requestEntity,
             requestRevision,
-            lookup -> {
-                final NiFiUser user = NiFiUserUtils.getNiFiUser();
-
-                final Authorizable groupAuthorizable = lookup.getProcessGroup(groupId).getAuthorizable();
-                groupAuthorizable.authorize(authorizer, RequestAction.WRITE, user);
-
-                // For every component that is affected, the user must have READ permissions and WRITE permissions
-                // (because this action requires stopping the component).
-                if (affectedProcessors != null) {
-                    for (final AffectedComponentDTO affectedComponent : affectedProcessors) {
-                        final Authorizable authorizable = lookup.getProcessor(affectedComponent.getComponentId()).getAuthorizable();
-                        authorizable.authorize(authorizer, RequestAction.READ, user);
-                        authorizable.authorize(authorizer, RequestAction.WRITE, user);
-                    }
-                }
-
-                if (affectedServices != null) {
-                    for (final AffectedComponentDTO affectedComponent : affectedServices) {
-                        final Authorizable authorizable = lookup.getControllerService(affectedComponent.getComponentId()).getAuthorizable();
-                        authorizable.authorize(authorizer, RequestAction.READ, user);
-                        authorizable.authorize(authorizer, RequestAction.WRITE, user);
-                    }
-                }
-            },
+            authorizeAccess,
             null,
-            (revision, varRegistryEntity) -> {
-                return updateVariableRegistryLocal(groupId, affectedProcessors, affectedServices, requestEntity);
-            });
+            (revision, varRegistryEntity) -> updateVariableRegistryLocal(groupId, allAffectedComponents, activeAffectedProcessors, activeAffectedServices, user, requestEntity)
+        );
     }
 
     private Pause createPause(final VariableRegistryUpdateRequest updateRequest) {
@@ -739,58 +757,49 @@ public class ProcessGroupResource extends ApplicationResource {
     }
 
     private void updateVariableRegistryReplicated(final String groupId, final URI originalUri, final Collection<AffectedComponentDTO> affectedProcessors,
-        final Collection<AffectedComponentDTO> affectedServices,
-        final VariableRegistryUpdateRequest updateRequest, final VariableRegistryEntity requestEntity) {
-
-        final NiFiProperties properties = getProperties();
-        final Client jerseyClient = WebUtils.createClient(new DefaultClientConfig(), SslContextFactory.createSslContext(properties));
-        final int connectionTimeout = (int) FormatUtils.getTimeDuration(properties.getClusterNodeConnectionTimeout(), TimeUnit.MILLISECONDS);
-        final int readTimeout = (int) FormatUtils.getTimeDuration(properties.getClusterNodeReadTimeout(), TimeUnit.MILLISECONDS);
-        jerseyClient.getProperties().put(ClientConfig.PROPERTY_CONNECT_TIMEOUT, connectionTimeout);
-        jerseyClient.getProperties().put(ClientConfig.PROPERTY_READ_TIMEOUT, readTimeout);
-        jerseyClient.getProperties().put(ClientConfig.PROPERTY_FOLLOW_REDIRECTS, Boolean.TRUE);
+                                                  final Collection<AffectedComponentDTO> affectedServices, final VariableRegistryUpdateRequest updateRequest,
+                                                  final VariableRegistryEntity requestEntity) throws InterruptedException, IOException {
 
         final Pause pause = createPause(updateRequest);
 
         // stop processors
         if (affectedProcessors != null) {
-            logger.info("In order to update Variable Registry for Process Group with ID {}, "
-                + "replicating request to stop {} affected processors", groupId, affectedProcessors.size());
-
-            scheduleProcessors(groupId, originalUri, jerseyClient, updateRequest, pause,
-                affectedProcessors, ScheduledState.STOPPED, updateRequest.getStopProcessorsStep());
+            logger.info("In order to update Variable Registry for Process Group with ID {}, replicating request to stop {} affected Processors", groupId, affectedProcessors.size());
+            scheduleProcessors(groupId, originalUri, updateRequest, pause, affectedProcessors, ScheduledState.STOPPED, updateRequest.getStopProcessorsStep());
+        } else {
+            logger.info("In order to update Variable Registry for Process Group with ID {}, no Processors are affected.", groupId);
+            updateRequest.getStopProcessorsStep().setComplete(true);
         }
 
         // disable controller services
         if (affectedServices != null) {
-            logger.info("In order to update Variable Registry for Process Group with ID {}, "
-                + "replicating request to stop {} affected Controller Services", groupId, affectedServices.size());
-
-            activateControllerServices(groupId, originalUri, jerseyClient, updateRequest, pause,
-                affectedServices, ControllerServiceState.DISABLED, updateRequest.getDisableServicesStep());
+            logger.info("In order to update Variable Registry for Process Group with ID {}, replicating request to stop {} affected Controller Services", groupId, affectedServices.size());
+            activateControllerServices(groupId, originalUri, updateRequest, pause, affectedServices, ControllerServiceState.DISABLED, updateRequest.getDisableServicesStep());
+        } else {
+            logger.info("In order to update Variable Registry for Process Group with ID {}, no Controller Services are affected.", groupId);
+            updateRequest.getDisableServicesStep().setComplete(true);
         }
 
         // apply updates
-        logger.info("In order to update Variable Registry for Process Group with ID {}, "
-            + "replicating request to apply updates to variable registry", groupId);
-        applyVariableRegistryUpdate(groupId, originalUri, jerseyClient, updateRequest, requestEntity);
+        logger.info("In order to update Variable Registry for Process Group with ID {}, replicating request to apply updates to variable registry", groupId);
+        applyVariableRegistryUpdate(groupId, originalUri, updateRequest, requestEntity);
 
         // re-enable controller services
         if (affectedServices != null) {
-            logger.info("In order to update Variable Registry for Process Group with ID {}, "
-                + "replicating request to re-enable {} affected services", groupId, affectedServices.size());
-
-            activateControllerServices(groupId, originalUri, jerseyClient, updateRequest, pause,
-                affectedServices, ControllerServiceState.ENABLED, updateRequest.getEnableServicesStep());
+            logger.info("In order to update Variable Registry for Process Group with ID {}, replicating request to re-enable {} affected services", groupId, affectedServices.size());
+            activateControllerServices(groupId, originalUri, updateRequest, pause, affectedServices, ControllerServiceState.ENABLED, updateRequest.getEnableServicesStep());
+        } else {
+            logger.info("In order to update Variable Registry for Process Group with ID {}, no Controller Services are affected.", groupId);
+            updateRequest.getEnableServicesStep().setComplete(true);
         }
 
         // restart processors
         if (affectedProcessors != null) {
-            logger.info("In order to update Variable Registry for Process Group with ID {}, "
-                + "replicating request to restart {} affected processors", groupId, affectedProcessors.size());
-
-            scheduleProcessors(groupId, originalUri, jerseyClient, updateRequest, pause,
-                affectedProcessors, ScheduledState.RUNNING, updateRequest.getStartProcessorsStep());
+            logger.info("In order to update Variable Registry for Process Group with ID {}, replicating request to restart {} affected processors", groupId, affectedProcessors.size());
+            scheduleProcessors(groupId, originalUri, updateRequest, pause, affectedProcessors, ScheduledState.RUNNING, updateRequest.getStartProcessorsStep());
+        } else {
+            logger.info("In order to update Variable Registry for Process Group with ID {}, no Processors are affected.", groupId);
+            updateRequest.getStartProcessorsStep().setComplete(true);
         }
 
         updateRequest.setComplete(true);
@@ -799,34 +808,45 @@ public class ProcessGroupResource extends ApplicationResource {
     /**
      * Periodically polls the process group with the given ID, waiting for all processors whose ID's are given to have the given Scheduled State.
      *
-     * @param client the Jersey Client to use for making the request
      * @param groupId the ID of the Process Group to poll
      * @param processorIds the ID of all Processors whose state should be equal to the given desired state
      * @param desiredState the desired state for all processors with the ID's given
      * @param pause the Pause that can be used to wait between polling
      * @return <code>true</code> if successful, <code>false</code> if unable to wait for processors to reach the desired state
      */
-    private boolean waitForProcessorStatus(final Client client, final URI originalUri, final String groupId, final Set<String> processorIds, final ScheduledState desiredState, final Pause pause) {
+    private boolean waitForProcessorStatus(final URI originalUri, final String groupId, final Set<String> processorIds, final ScheduledState desiredState,
+                                           final VariableRegistryUpdateRequest updateRequest, final Pause pause) throws InterruptedException {
         URI groupUri;
         try {
             groupUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
-                originalUri.getPort(), "/nifi-api/flow/process-groups/" + groupId + "/status", "recursive=true", originalUri.getFragment());
+                originalUri.getPort(), "/nifi-api/process-groups/" + groupId + "/processors", "includeDescendantGroups=true", originalUri.getFragment());
         } catch (URISyntaxException e) {
             throw new RuntimeException(e);
         }
 
+        final Map<String, String> headers = new HashMap<>();
+        final MultivaluedMap<String, String> requestEntity = new MultivaluedMapImpl();
+
         boolean continuePolling = true;
         while (continuePolling) {
-            final ClientResponse response = client.resource(groupUri).header("Content-Type", "application/json").get(ClientResponse.class);
-            if (response.getStatus() != Status.OK.getStatusCode()) {
+
+            // Determine whether we should replicate only to the cluster coordinator, or if we should replicate directly to the cluster nodes themselves.
+            final NodeResponse clusterResponse;
+            if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                clusterResponse = getRequestReplicator().replicate(HttpMethod.GET, groupUri, requestEntity, headers).awaitMergedResponse();
+            } else {
+                clusterResponse = getRequestReplicator().forwardToCoordinator(
+                        getClusterCoordinatorNode(), HttpMethod.GET, groupUri, requestEntity, headers).awaitMergedResponse();
+            }
+
+            if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
                 return false;
             }
 
-            final ProcessGroupStatusEntity statusEntity = response.getEntity(ProcessGroupStatusEntity.class);
-            final ProcessGroupStatusDTO statusDto = statusEntity.getProcessGroupStatus();
-            final ProcessGroupStatusSnapshotDTO statusSnapshotDto = statusDto.getAggregateSnapshot();
+            final ProcessorsEntity processorsEntity = getResponseEntity(clusterResponse, ProcessorsEntity.class);
+            final Set<ProcessorEntity> processorEntities = processorsEntity.getProcessors();
 
-            if (isProcessorStatusEqual(statusSnapshotDto, processorIds, desiredState)) {
+            if (isProcessorActionComplete(processorEntities, updateRequest, processorIds, desiredState)) {
                 logger.debug("All {} processors of interest now have the desired state of {}", processorIds.size(), desiredState);
                 return true;
             }
@@ -847,14 +867,14 @@ public class ProcessGroupResource extends ApplicationResource {
      * @param pause the Pause that can be used to wait between polling
      * @return <code>true</code> if successful, <code>false</code> if unable to wait for processors to reach the desired state
      */
-    private boolean waitForLocalProcessorStatus(final String groupId, final Set<String> processorIds, final ScheduledState desiredState, final Pause pause) {
+    private boolean waitForLocalProcessor(final String groupId, final Set<String> processorIds, final ScheduledState desiredState,
+                                          final VariableRegistryUpdateRequest updateRequest, final Pause pause) {
+
         boolean continuePolling = true;
         while (continuePolling) {
-            final ProcessGroupStatusEntity statusEntity = serviceFacade.getProcessGroupStatus(groupId, true);
-            final ProcessGroupStatusDTO statusDto = statusEntity.getProcessGroupStatus();
-            final ProcessGroupStatusSnapshotDTO statusSnapshotDto = statusDto.getAggregateSnapshot();
+            final Set<ProcessorEntity> processorEntities = serviceFacade.getProcessors(groupId, true);
 
-            if (isProcessorStatusEqual(statusSnapshotDto, processorIds, desiredState)) {
+            if (isProcessorActionComplete(processorEntities, updateRequest, processorIds, desiredState)) {
                 logger.debug("All {} processors of interest now have the desired state of {}", processorIds.size(), desiredState);
                 return true;
             }
@@ -866,55 +886,93 @@ public class ProcessGroupResource extends ApplicationResource {
         return false;
     }
 
-    private boolean isProcessorStatusEqual(final ProcessGroupStatusSnapshotDTO statusSnapshot, final Set<String> processorIds, final ScheduledState desiredState) {
+    private boolean isProcessorActionComplete(final Set<ProcessorEntity> processorEntities, final VariableRegistryUpdateRequest updateRequest,
+                                              final Set<String> processorIds, final ScheduledState desiredState) {
+
         final String desiredStateName = desiredState.name();
 
-        final boolean allProcessorsMatch = statusSnapshot.getProcessorStatusSnapshots().stream()
-            .map(entity -> entity.getProcessorStatusSnapshot())
-            .filter(status -> processorIds.contains(status.getId()))
-            .allMatch(status -> {
-                final String runStatus = status.getRunStatus();
-                final boolean stateMatches = desiredStateName.equalsIgnoreCase(runStatus);
-                if (!stateMatches) {
-                    return false;
-                }
+        // update the affected processors
+        processorEntities.stream()
+                .filter(entity -> updateRequest.getAffectedComponents().containsKey(entity.getId()))
+                .forEach(entity -> {
+                    final AffectedComponentEntity affectedComponentEntity = updateRequest.getAffectedComponents().get(entity.getId());
+                    affectedComponentEntity.setRevision(entity.getRevision());
+
+                    // only consider update this component if the user had permissions to it
+                    if (Boolean.TRUE.equals(affectedComponentEntity.getPermissions().getCanRead())) {
+                        final AffectedComponentDTO affectedComponent = affectedComponentEntity.getComponent();
+                        affectedComponent.setState(entity.getStatus().getAggregateSnapshot().getRunStatus());
+                        affectedComponent.setActiveThreadCount(entity.getStatus().getAggregateSnapshot().getActiveThreadCount());
+
+                        if (Boolean.TRUE.equals(entity.getPermissions().getCanRead())) {
+                            affectedComponent.setValidationErrors(entity.getComponent().getValidationErrors());
+                        }
+                    }
+                });
 
-                if (desiredState == ScheduledState.STOPPED && status.getActiveThreadCount() != 0) {
-                    return false;
-                }
+        final boolean allProcessorsMatch = processorEntities.stream()
+                .filter(entity -> processorIds.contains(entity.getId()))
+                .allMatch(entity -> {
+                    final ProcessorStatusDTO status = entity.getStatus();
 
-                return true;
-            });
+                    final String runStatus = status.getAggregateSnapshot().getRunStatus();
+                    final boolean stateMatches = desiredStateName.equalsIgnoreCase(runStatus);
+                    if (!stateMatches) {
+                        return false;
+                    }
+
+                    if (desiredState == ScheduledState.STOPPED && status.getAggregateSnapshot().getActiveThreadCount() != 0) {
+                        return false;
+                    }
+
+                    return true;
+                });
 
         if (!allProcessorsMatch) {
             return false;
         }
 
-        for (final ProcessGroupStatusSnapshotEntity childGroupEntity : statusSnapshot.getProcessGroupStatusSnapshots()) {
-            final ProcessGroupStatusSnapshotDTO childGroupStatus = childGroupEntity.getProcessGroupStatusSnapshot();
-            final boolean allMatchChildLevel = isProcessorStatusEqual(childGroupStatus, processorIds, desiredState);
-            if (!allMatchChildLevel) {
-                return false;
-            }
-        }
-
         return true;
     }
 
-
+    /**
+     * Updates the affected controller services in the specified updateRequest with the serviceEntities.
+     *
+     * @param serviceEntities service entities
+     * @param updateRequest update request
+     */
+    private void updateAffectedControllerServices(final Set<ControllerServiceEntity> serviceEntities, final VariableRegistryUpdateRequest updateRequest) {
+        // update the affected components
+        serviceEntities.stream()
+                .filter(entity -> updateRequest.getAffectedComponents().containsKey(entity.getId()))
+                .forEach(entity -> {
+                    final AffectedComponentEntity affectedComponentEntity = updateRequest.getAffectedComponents().get(entity.getId());
+                    affectedComponentEntity.setRevision(entity.getRevision());
+
+                    // only consider update this component if the user had permissions to it
+                    if (Boolean.TRUE.equals(affectedComponentEntity.getPermissions().getCanRead())) {
+                        final AffectedComponentDTO affectedComponent = affectedComponentEntity.getComponent();
+                        affectedComponent.setState(entity.getComponent().getState());
+
+                        if (Boolean.TRUE.equals(entity.getPermissions().getCanRead())) {
+                            affectedComponent.setValidationErrors(entity.getComponent().getValidationErrors());
+                        }
+                    }
+                });
+    }
 
     /**
      * Periodically polls the process group with the given ID, waiting for all controller services whose ID's are given to have the given Controller Service State.
      *
-     * @param client the Jersey Client to use for making the HTTP Request
      * @param groupId the ID of the Process Group to poll
      * @param serviceIds the ID of all Controller Services whose state should be equal to the given desired state
      * @param desiredState the desired state for all services with the ID's given
      * @param pause the Pause that can be used to wait between polling
      * @return <code>true</code> if successful, <code>false</code> if unable to wait for services to reach the desired state
      */
-    private boolean waitForControllerServiceStatus(final Client client, final URI originalUri, final String groupId, final Set<String> serviceIds, final ControllerServiceState desiredState,
-        final Pause pause) {
+    private boolean waitForControllerServiceStatus(final URI originalUri, final String groupId, final Set<String> serviceIds,
+                                                   final ControllerServiceState desiredState, final VariableRegistryUpdateRequest updateRequest, final Pause pause) throws InterruptedException {
+
         URI groupUri;
         try {
             groupUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
@@ -923,16 +981,31 @@ public class ProcessGroupResource extends ApplicationResource {
             throw new RuntimeException(e);
         }
 
+        final Map<String, String> headers = new HashMap<>();
+        final MultivaluedMap<String, String> requestEntity = new MultivaluedMapImpl();
+
         boolean continuePolling = true;
         while (continuePolling) {
-            final ClientResponse response = client.resource(groupUri).header("Content-Type", "application/json").get(ClientResponse.class);
-            if (response.getStatus() != Status.OK.getStatusCode()) {
+
+            // Determine whether we should replicate only to the cluster coordinator, or if we should replicate directly to the cluster nodes themselves.
+            final NodeResponse clusterResponse;
+            if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+                clusterResponse = getRequestReplicator().replicate(HttpMethod.GET, groupUri, requestEntity, headers).awaitMergedResponse();
+            } else {
+                clusterResponse = getRequestReplicator().forwardToCoordinator(
+                        getClusterCoordinatorNode(), HttpMethod.GET, groupUri, requestEntity, headers).awaitMergedResponse();
+            }
+
+            if (clusterResponse.getStatus() != Status.OK.getStatusCode()) {
                 return false;
             }
 
-            final ControllerServicesEntity controllerServicesEntity = response.getEntity(ControllerServicesEntity.class);
+            final ControllerServicesEntity controllerServicesEntity = getResponseEntity(clusterResponse, ControllerServicesEntity.class);
             final Set<ControllerServiceEntity> serviceEntities = controllerServicesEntity.getControllerServices();
 
+            // update the affected controller services
+            updateAffectedControllerServices(serviceEntities, updateRequest);
+
             final String desiredStateName = desiredState.name();
             final boolean allServicesMatch = serviceEntities.stream()
                 .map(entity -> entity.getComponent())
@@ -963,11 +1036,16 @@ public class ProcessGroupResource extends ApplicationResource {
      * @param user the user that is retrieving the controller services
      * @return <code>true</code> if successful, <code>false</code> if unable to wait for services to reach the desired state
      */
-    private boolean waitForLocalControllerServiceStatus(final String groupId, final Set<String> serviceIds, final ControllerServiceState desiredState, final Pause pause, final NiFiUser user) {
+    private boolean waitForLocalControllerServiceStatus(final String groupId, final Set<String> serviceIds, final ControllerServiceState desiredState,
+                                                        final VariableRegistryUpdateRequest updateRequest, final Pause pause, final NiFiUser user) {
+
         boolean continuePolling = true;
         while (continuePolling) {
             final Set<ControllerServiceEntity> serviceEntities = serviceFacade.getControllerServices(groupId, false, true, user);
 
+            // update the affected controller services
+            updateAffectedControllerServices(serviceEntities, updateRequest);
+
             final String desiredStateName = desiredState.name();
             final boolean allServicesMatch = serviceEntities.stream()
                 .map(entity -> entity.getComponent())
@@ -975,6 +1053,7 @@ public class ProcessGroupResource extends ApplicationResource {
                 .map(service -> service.getState())
                 .allMatch(state -> desiredStateName.equals(state));
 
+
             if (allServicesMatch) {
                 logger.debug("All {} controller services of interest now have the desired state of {}", serviceIds.size(), desiredState);
                 return true;
@@ -987,8 +1066,8 @@ public class ProcessGroupResource extends ApplicationResource {
         return false;
     }
 
-    private VariableRegistryUpdateRequest createVariableRegistryUpdateRequest(final String groupId) {
-        final VariableRegistryUpdateRequest updateRequest = new VariableRegistryUpdateRequest(UUID.randomUUID().toString(), groupId);
+    private VariableRegistryUpdateRequest createVariableRegistryUpdateRequest(final String groupId, final Set<AffectedComponentEntity> affectedComponents, final NiFiUser user) {
+        final VariableRegistryUpdateRequest updateRequest = new VariableRegistryUpdateRequest(UUID.randomUUID().toString(), groupId, affectedComponents, user);
 
         // before adding to the request map, purge any old requests. Must do this by creating a List of ID's
         // and then removing those ID's one-at-a-time in order to avoid ConcurrentModificationException.
@@ -1011,27 +1090,26 @@ public class ProcessGroupResource extends ApplicationResource {
         return updateRequest;
     }
 
-    private Response updateVariableRegistryLocal(final String groupId, final List<AffectedComponentDTO> affectedProcessors, final List<AffectedComponentDTO> affectedServices,
-        final VariableRegistryEntity requestEntity) {
+    private Response updateVariableRegistryLocal(final String groupId, final Set<AffectedComponentEntity> affectedComponents, final List<AffectedComponentDTO> affectedProcessors,
+                                                 final List<AffectedComponentDTO> affectedServices, final NiFiUser user, final VariableRegistryEntity requestEntity) {
 
         final Set<String> affectedProcessorIds = affectedProcessors == null ? Collections.emptySet() : affectedProcessors.stream()
-            .map(component -> component.getComponentId())
+            .map(component -> component.getId())
             .collect(Collectors.toSet());
         Map<String, Revision> processorRevisionMap = getRevisions(groupId, affectedProcessorIds);
 
         final Set<String> affectedServiceIds = affectedServices == null ? Collections.emptySet() : affectedServices.stream()
-            .map(component -> component.getComponentId())
+            .map(component -> component.getId())
             .collect(Collectors.toSet());
         Map<String, Revision> serviceRevisionMap = getRevisions(groupId, affectedServiceIds);
 
         // update the variable registry
-        final VariableRegistryUpdateRequest updateRequest = createVariableRegistryUpdateRequest(groupId);
+        final VariableRegistryUpdateRequest updateRequest = createVariableRegistryUpdateRequest(groupId, affectedComponents, user);
         updateRequest.getIdentifyRelevantComponentsStep().setComplete(true);
         final Pause pause = createPause(updateRequest);
 
         final Revision requestRevision = getRevision(requestEntity.getProcessGroupRevision(), groupId);
 
-        final NiFiUser user = NiFiUserUtils.getNiFiUser();
         final Runnable updateTask = new Runnable() {
             @Override
             public void run() {
@@ -1052,21 +1130,26 @@ public class ProcessGroupResource extends ApplicationResource {
 
                     // Apply the updates
                     performUpdateVariableRegistryStep(groupId, updateRequest, updateRequest.getApplyUpdatesStep(), "Applying updates to Variable Registry",
-                        () -> serviceFacade.updateVariableRegistry(user, requestRevision, requestEntity.getVariableRegistry()));
+                        () -> {
+                            final VariableRegistryEntity entity = serviceFacade.updateVariableRegistry(user, requestRevision, requestEntity.getVariableRegistry());
+                            updateRequest.setProcessGroupRevision(entity.getProcessGroupRevision());
+                        });
 
                     // Re-enable the controller services
                     performUpdateVariableRegistryStep(groupId, updateRequest, updateRequest.getEnableServicesStep(), "Re-enabling Controller Services",
-                        () -> enableControllerServices(user, groupId, updatedServiceRevisionMap, pause));
+                        () -> enableControllerServices(user, updateRequest, groupId, updatedServiceRevisionMap, pause));
 
                     // Restart processors
                     performUpdateVariableRegistryStep(groupId, updateRequest, updateRequest.getStartProcessorsStep(), "Restarting Processors",
-                        () -> startProcessors(user, groupId, updatedProcessorRevisionMap, pause));
+                        () -> startProcessors(user, updateRequest, groupId, updatedProcessorRevisionMap, pause));
 
                     // Set complete
                     updateRequest.setComplete(true);
                     updateRequest.setLastUpdated(new Date());
                 } catch (final Exception e) {
                     logger.error("Failed to update Variable Registry for Proces Group with ID " + groupId, e);
+
+                    updateRequest.setComplete(true);
                     updateRequest.setFailureReason("An unexpected error has occurred: " + e);
                 }
             }
@@ -1076,10 +1159,11 @@ public class ProcessGroupResource extends ApplicationResource {
         variableRegistryThreadPool.submit(updateTask);
 
         final VariableRegistryUpdateRequestEntity responseEntity = new VariableRegistryUpdateRequestEntity();
-        responseEntity.setRequestDto(dtoFactory.createVariableRegistryUpdateRequestDto(updateRequest));
-        responseEntity.getRequestDto().setUri(generateResourceUri("process-groups", groupId, "variable-registry", "update-requests", updateRequest.getRequestId()));
+        responseEntity.setRequest(dtoFactory.createVariableRegistryUpdateRequestDto(updateRequest));
+        responseEntity.setProcessGroupRevision(updateRequest.getProcessGroupRevision());
+        responseEntity.getRequest().setUri(generateResourceUri("process-groups", groupId, "variable-registry", "update-requests", updateRequest.getRequestId()));
 
-        final URI location = URI.create(responseEntity.getRequestDto().getUri());
+        final URI location = URI.create(responseEntity.getRequest().getUri());
         return Response.status(Status.ACCEPTED).location(location).entity(responseEntity).build();
     }
 
@@ -1103,11 +1187,12 @@ public class ProcessGroupResource extends ApplicationResource {
             action.run();
             step.setComplete(true);
         } catch (final Exception e) {
-            request.setComplete(true);
             logger.error("Failed to update variable registry for Process Group with ID {}", groupId, e);
 
             step.setComplete(true);
             step.setFailureReason(e.getMessage());
+
+            request.setComplete(true);
             request.setFailureReason("Failed to update Variable Registry because failed while performing step: " + stepDescription);
         }
 
@@ -1123,21 +1208,21 @@ public class ProcessGroupResource extends ApplicationResource {
 
         serviceFacade.verifyScheduleComponents(processGroupId, ScheduledState.STOPPED, processorRevisions.keySet());
         serviceFacade.scheduleComponents(user, processGroupId, ScheduledState.STOPPED, processorRevisions);
-        waitForLocalProcessorStatus(processGroupId, processorRevisions.keySet(), ScheduledState.STOPPED, pause);
+        waitForLocalProcessor(processGroupId, processorRevisions.keySet(), ScheduledState.STOPPED, updateRequest, pause);
     }
 
-    private void startProcessors(final NiFiUser user, final String processGroupId, final Map<String, Revision> processorRevisions, final Pause pause) {
+    private void startProcessors(final NiFiUser user, final VariableRegistryUpdateRequest request, final String processGroupId, final Map<String, Revision> processorRevisions, final Pause pause) {
         if (processorRevisions.isEmpty()) {
             return;
         }
 
         serviceFacade.verifyScheduleComponents(processGroupId, ScheduledState.RUNNING, processorRevisions.keySet());
         serviceFacade.scheduleComponents(user, processGroupId, ScheduledState.RUNNING, processorRevisions);
-        waitForLocalProcessorStatus(processGroupId, processorRevisions.keySet(), ScheduledState.RUNNING, pause);
+        waitForLocalProcessor(processGroupId, processorRevisions.keySet(), ScheduledState.RUNNING, request, pause);
     }
 
     private void disableControllerServices(final NiFiUser user, final VariableRegistryUpdateRequest updateRequest, final String processGroupId,
-        final Map<String, Revision> serviceRevisions, final Pause pause) {
+                                           final Map<String, Revision> serviceRevisions, final Pause pause) {
 
         if (serviceRevisions.isEmpty()) {
             return;
@@ -1145,116 +1230,141 @@ public class ProcessGroupResource extends ApplicationResource {
 
         serviceFacade.verifyActivateControllerServices(processGroupId, ControllerServiceState.DISABLED, serviceRevisions.keySet());
         serviceFacade.activateControllerServices(user, processGroupId, ControllerServiceState.DISABLED, serviceRevisions);
-        waitForLocalControllerServiceStatus(processGroupId, serviceRevisions.keySet(), ControllerServiceState.DISABLED, pause, user);
+        waitForLocalControllerServiceStatus(processGroupId, serviceRevisions.keySet(), ControllerServiceState.DISABLED, updateRequest, pause, user);
     }
 
-    private void enableControllerServices(final NiFiUser user, final String processGroupId, final Map<String, Revision> serviceRevisions, final Pause pause) {
+    private void enableControllerServices(final NiFiUser user, final VariableRegistryUpdateRequest updateRequest, final String processGroupId,
+                                          final Map<String, Revision> serviceRevisions, final Pause pause) {
+
         if (serviceRevisions.isEmpty()) {
             return;
         }
 
         serviceFacade.verifyActivateControllerServices(processGroupId, ControllerServiceState.ENABLED, serviceRevisions.keySet());
         serviceFacade.activateControllerServices(user, processGroupId, ControllerServiceState.ENABLED, serviceRevisions);
-        waitForLocalControllerServiceStatus(processGroupId, serviceRevisions.keySet(), ControllerServiceState.ENABLED, pause, user);
+        waitForLocalControllerServiceStatus(processGroupId, serviceRevisions.keySet(), ControllerServiceState.ENABLED, updateRequest, pause, user);
     }
 
 
-    private void scheduleProcessors(final String groupId, final URI originalUri, final Client jerseyClient, final VariableRegistryUpdateRequest updateRequest,
-        final Pause pause, final Collection<AffectedComponentDTO> affectedProcessors, final ScheduledState desiredState, final VariableRegistryUpdateStep updateStep) {
+    private void scheduleProcessors(final String groupId, final URI originalUri, final VariableRegistryUpdateRequest updateRequest,
+                                    final Pause pause, final Collection<AffectedComponentDTO> affectedProcessors, final ScheduledState desiredState,
+                                    final VariableRegistryUpdateStep updateStep) throws InterruptedException {
+
         final Set<String> affectedProcessorIds = affectedProcessors.stream()
-            .map(component -> component.getComponentId())
+            .map(component -> component.getId())
             .collect(Collectors.toSet());
 
         final Map<String, Revision> processorRevisionMap = getRevisions(groupId, affectedProcessorIds);
         final Map<String, RevisionDTO> processorRevisionDtoMap = processorRevisionMap.entrySet().stream().collect(
             Collectors.toMap(Map.Entry::getKey, entry -> dtoFactory.createRevisionDTO(entry.getValue())));
 
-        final ScheduleComponentsEntity stopProcessorsEntity = new ScheduleComponentsEntity();
-        stopProcessorsEntity.setComponents(processorRevisionDtoMap);
-        stopProcessorsEntity.setId(groupId);
-        stopProcessorsEntity.setState(desiredState.name());
+        final ScheduleComponentsEntity scheduleProcessorsEntity = new ScheduleComponentsEntity();
+        scheduleProcessorsEntity.setComponents(processorRevisionDtoMap);
+        scheduleProcessorsEntity.setId(groupId);
+        scheduleProcessorsEntity.setState(desiredState.name());
 
-        URI stopProcessorUri;
+        URI scheduleGroupUri;
         try {
-            stopProcessorUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
+            scheduleGroupUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
                 originalUri.getPort(), "/nifi-api/flow/process-groups/" + groupId, null, originalUri.getFragment());
         } catch (URISyntaxException e) {
             throw new RuntimeException(e);
         }
 
-        final ClientResponse stopProcessorResponse = jerseyClient.resource(stopProcessorUri)
-            .header("Content-Type", "application/json")
-            .entity(stopProcessorsEntity)
-            .put(ClientResponse.class);
+        final Map<String, String> headers = new HashMap<>();
+        headers.put("content-type", MediaType.APPLICATION_JSON);
+
+        // Determine whether we should replicate only to the cluster coordinator, or if we should replicate directly to the cluster nodes themselves.
+        final NodeResponse clusterResponse;
+        if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+            clusterResponse = getRequestReplicator().replicate(HttpMethod.PUT, scheduleGroupUri, scheduleProcessorsEntity, headers).awaitMergedResponse();
+        } else {
+            clusterResponse = getRequestReplicator().forwardToCoordinator(
+                    getClusterCoordinatorNode(), HttpMethod.PUT, scheduleGroupUri, scheduleProcessorsEntity, headers).awaitMergedResponse();
+        }
 
-        final int stopProcessorStatus = stopProcessorResponse.getStatus();
+        final int stopProcessorStatus = clusterResponse.getStatus();
         if (stopProcessorStatus != Status.OK.getStatusCode()) {
             updateRequest.getStopProcessorsStep().setFailureReason("Failed while " + updateStep.getDescription());
+
+            updateStep.setComplete(true);
             updateRequest.setFailureReason("Failed while " + updateStep.getDescription());
             return;
         }
 
         updateRequest.setLastUpdated(new Date());
-        final boolean processorsTransitioned = waitForProcessorStatus(jerseyClient, originalUri, groupId, affectedProcessorIds, desiredState, pause);
-        if (processorsTransitioned) {
-            updateStep.setComplete(true);
-        } else {
+        final boolean processorsTransitioned = waitForProcessorStatus(originalUri, groupId, affectedProcessorIds, desiredState, updateRequest, pause);
+        updateStep.setComplete(true);
+
+        if (!processorsTransitioned) {
             updateStep.setFailureReason("Failed while " + updateStep.getDescription());
+
+            updateRequest.setComplete(true);
             updateRequest.setFailureReason("Failed while " + updateStep.getDescription());
-            return;
         }
     }
 
-    private void activateControllerServices(final String groupId, final URI originalUri, final Client jerseyClient, final VariableRegistryUpdateRequest updateRequest,
-        final Pause pause, final Collection<AffectedComponentDTO> affectedServices, final ControllerServiceState desiredState, final VariableRegistryUpdateStep updateStep) {
+    private void activateControllerServices(final String groupId, final URI originalUri, final VariableRegistryUpdateRequest updateRequest,
+        final Pause pause, final Collection<AffectedComponentDTO> affectedServices, final ControllerServiceState desiredState, final VariableRegistryUpdateStep updateStep) throws InterruptedException {
 
         final Set<String> affectedServiceIds = affectedServices.stream()
-            .map(component -> component.getComponentId())
+            .map(component -> component.getId())
             .collect(Collectors.toSet());
 
         final Map<String, Revision> serviceRevisionMap = getRevisions(groupId, affectedServiceIds);
         final Map<String, RevisionDTO> serviceRevisionDtoMap = serviceRevisionMap.entrySet().stream().collect(
             Collectors.toMap(Map.Entry::getKey, entry -> dtoFactory.createRevisionDTO(entry.getValue())));
 
-        final ActivateControllerServicesEntity disableServicesEntity = new ActivateControllerServicesEntity();
-        disableServicesEntity.setComponents(serviceRevisionDtoMap);
-        disableServicesEntity.setId(groupId);
-        disableServicesEntity.setState(desiredState.name());
+        final ActivateControllerServicesEntity activateServicesEntity = new ActivateControllerServicesEntity();
+        activateServicesEntity.setComponents(serviceRevisionDtoMap);
+        activateServicesEntity.setId(groupId);
+        activateServicesEntity.setState(desiredState.name());
 
-        URI disableServicesUri;
+        URI controllerServicesUri;
         try {
-            disableServicesUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
+            controllerServicesUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
                 originalUri.getPort(), "/nifi-api/flow/process-groups/" + groupId + "/controller-services", null, originalUri.getFragment());
         } catch (URISyntaxException e) {
             throw new RuntimeException(e);
         }
 
-        final ClientResponse disableServicesResponse = jerseyClient.resource(disableServicesUri)
-            .header("Content-Type", "application/json")
-            .entity(disableServicesEntity)
-            .put(ClientResponse.class);
+        final Map<String, String> headers = new HashMap<>();
+        headers.put("content-type", MediaType.APPLICATION_JSON);
 
-        final int disableServicesStatus = disableServicesResponse.getStatus();
+        // Determine whether we should replicate only to the cluster coordinator, or if we should replicate directly to the cluster nodes themselves.
+        final NodeResponse clusterResponse;
+        if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+            clusterResponse = getRequestReplicator().replicate(HttpMethod.PUT, controllerServicesUri, activateServicesEntity, headers).awaitMergedResponse();
+        } else {
+            clusterResponse = getRequestReplicator().forwardToCoordinator(
+                    getClusterCoordinatorNode(), HttpMethod.PUT, controllerServicesUri, activateServicesEntity, headers).awaitMergedResponse();
+        }
+
+        final int disableServicesStatus = clusterResponse.getStatus();
         if (disableServicesStatus != Status.OK.getStatusCode()) {
             updateStep.setFailureReason("Failed while " + updateStep.getDescription());
+
+            updateStep.setComplete(true);
             updateRequest.setFailureReason("Failed while " + updateStep.getDescription());
             return;
         }
 
         updateRequest.setLastUpdated(new Date());
-        if (waitForControllerServiceStatus(jerseyClient, originalUri, groupId, affectedServiceIds, desiredState, pause)) {
-            updateStep.setComplete(true);
-        } else {
+        final boolean serviceTransitioned = waitForControllerServiceStatus(originalUri, groupId, affectedServiceIds, desiredState, updateRequest, pause);
+        updateStep.setComplete(true);
+
+        if (!serviceTransitioned) {
             updateStep.setFailureReason("Failed while " + updateStep.getDescription());
+
+            updateRequest.setComplete(true);
             updateRequest.setFailureReason("Failed while " + updateStep.getDescription());
-            return;
         }
     }
 
+    private void applyVariableRegistryUpdate(final String groupId, final URI originalUri, final VariableRegistryUpdateRequest updateRequest,
+        final VariableRegistryEntity updateEntity) throws InterruptedException, IOException {
 
-    private void applyVariableRegistryUpdate(final String groupId, final URI originalUri, final Client jerseyClient, final VariableRegistryUpdateRequest updateRequest,
-        final VariableRegistryEntity updateEntity) {
-
+        // convert request accordingly
         URI applyUpdatesUri;
         try {
             applyUpdatesUri = new URI(originalUri.getScheme(), originalUri.getUserInfo(), originalUri.getHost(),
@@ -1263,21 +1373,53 @@ public class ProcessGroupResource extends ApplicationResource {
             throw new RuntimeException(e);
         }
 
-        final ClientResponse applyUpdatesResponse = jerseyClient.resource(applyUpdatesUri)
-            .header("Content-Type", "application/json")
-            .entity(updateEntity)
-            .put(ClientResponse.class);
+        final Map<String, String> headers = new HashMap<>();
+        headers.put("content-type", MediaType.APPLICATION_JSON);
 
-        final int applyUpdatesStatus = applyUpdatesResponse.getStatus();
+        // Determine whether we should replicate only to the cluster coordinator, or if we should replicate directly to the cluster nodes themselves.
+        final NodeResponse clusterResponse;
+        if (getReplicationTarget() == ReplicationTarget.CLUSTER_NODES) {
+            clusterResponse = getRequestReplicator().replicate(HttpMethod.PUT, applyUpdatesUri, updateEntity, headers).awaitMergedResponse();
+        } else {
+            clusterResponse = getRequestReplicator().forwardToCoordinator(
+                    getClusterCoordinatorNode(), HttpMethod.PUT, applyUpdatesUri, updateEntity, headers).awaitMergedResponse();
+        }
+
+        final int applyUpdatesStatus = clusterResponse.getStatus();
         updateRequest.setLastUpdated(new Date());
-        if (applyUpdatesStatus != Status.OK.getStatusCode()) {
-            updateRequest.getApplyUpdatesStep().setFailureReason("Failed to apply updates to the Variable Registry");
-            updateRequest.setFailureReason("Failed to apply updates to the Variable Registry");
-            return;
+        updateRequest.getApplyUpdatesStep().setComplete(true);
+
+        if (applyUpdatesStatus == Status.OK.getStatusCode()) {
+            // grab the current process group revision
+            final VariableRegistryEntity entity = getResponseEntity(clusterResponse, VariableRegistryEntity.class);
+            updateRequest.setProcessGroupRevision(entity.getProcessGroupRevision());
+        } else {
+            final String message = getResponseEntity(clusterResponse, String.class);
+
+            // update the request progress
+            updateRequest.getApplyUpdatesStep().setFailureReason("Failed to apply updates to the Variable Registry: " + message);
+            updateRequest.setComplete(true);
+            updateRequest.setFailureReason("Failed to apply updates to the Variable Registry: " + message);
         }
     }
 
     /**
+     * Extracts the response entity from the specified node response.
+     *
+     * @param nodeResponse node response
+     * @param clazz class
+     * @param <T> type of class
+     * @return the response entity
+     */
+    private <T> T getResponseEntity(final NodeResponse nodeResponse, final Class<T> clazz) {
+        T entity = (T) nodeResponse.getUpdatedEntity();
+        if (entity == null) {
+            entity = nodeResponse.getClientResponse().getEntity(clazz);
+        }
+        return entity;
+    }
+
+    /**
      * Removes the specified process group reference.
      *
      * @param httpServletRequest request
@@ -1676,7 +1818,8 @@ public class ProcessGroupResource extends ApplicationResource {
                     value = "The process group id.",
                     required = true
             )
-            @PathParam("id") final String groupId) {
+            @PathParam("id") final String groupId,
+            @ApiParam("Whether or not to include processors from descendant process groups") @QueryParam("includeDescendantGroups") @DefaultValue("false") boolean includeDescendantGroups) {
 
         if (isReplicateRequest()) {
             return replicate(HttpMethod.GET);
@@ -1689,7 +1832,7 @@ public class ProcessGroupResource extends ApplicationResource {
         });
 
         // get the processors
-        final Set<ProcessorEntity> processors = serviceFacade.getProcessors(groupId);
+        final Set<ProcessorEntity> processors = serviceFacade.getProcessors(groupId, includeDescendantGroups);
 
         // create the response entity
         final ProcessorsEntity entity = new ProcessorsEntity();
@@ -3121,7 +3264,6 @@ public class ProcessGroupResource extends ApplicationResource {
             uriBuilder.segment("process-groups", groupId, "templates", "import");
             final URI importUri = uriBuilder.build();
 
-            // change content type to XML for serializing entity
             final Map<String, String> headersToOverride = new HashMap<>();
             headersToOverride.put("content-type", MediaType.APPLICATION_XML);
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index ed42e9f..1a4d52b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -16,33 +16,6 @@
  */
 package org.apache.nifi.web.api.dto;
 
-import java.text.Collator;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TimeZone;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-import javax.ws.rs.WebApplicationException;
-
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.action.Action;
@@ -182,6 +155,7 @@ import org.apache.nifi.web.api.dto.status.RemoteProcessGroupStatusDTO;
 import org.apache.nifi.web.api.dto.status.RemoteProcessGroupStatusSnapshotDTO;
 import org.apache.nifi.web.api.entity.AccessPolicyEntity;
 import org.apache.nifi.web.api.entity.AccessPolicySummaryEntity;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.AllowableValueEntity;
 import org.apache.nifi.web.api.entity.BulletinEntity;
 import org.apache.nifi.web.api.entity.ComponentReferenceEntity;
@@ -196,6 +170,32 @@ import org.apache.nifi.web.api.entity.VariableEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
 import org.apache.nifi.web.revision.RevisionManager;
 
+import javax.ws.rs.WebApplicationException;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
 public final class DtoFactory {
 
     @SuppressWarnings("rawtypes")
@@ -1737,13 +1737,29 @@ public final class DtoFactory {
 
     public AffectedComponentDTO createAffectedComponentDto(final ConfiguredComponent component) {
         final AffectedComponentDTO dto = new AffectedComponentDTO();
-        dto.setComponentId(component.getIdentifier());
-        dto.setParentGroupId(component.getProcessGroupIdentifier());
+        dto.setId(component.getIdentifier());
+        dto.setName(component.getName());
+        dto.setProcessGroupId(component.getProcessGroupIdentifier());
 
         if (component instanceof ProcessorNode) {
-            dto.setComponentType(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR);
+            final ProcessorNode node = ((ProcessorNode) component);
+            dto.setState(node.getScheduledState().name());
+            dto.setActiveThreadCount(node.getActiveThreadCount());
+            dto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR);
         } else if (component instanceof ControllerServiceNode) {
-            dto.setComponentType(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE);
+            final ControllerServiceNode node = ((ControllerServiceNode) component);
+            dto.setState(node.getState().name());
+            dto.setReferenceType(AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE);
+        }
+
+        final Collection<ValidationResult> validationErrors = component.getValidationErrors();
+        if (validationErrors != null && !validationErrors.isEmpty()) {
+            final List<String> errors = new ArrayList<>();
+            for (final ValidationResult validationResult : validationErrors) {
+                errors.add(validationResult.toString());
+            }
+
+            dto.setValidationErrors(errors);
         }
 
         return dto;
@@ -2114,8 +2130,18 @@ public final class DtoFactory {
         return deprecationNotice == null ? null : deprecationNotice.reason();
     }
 
+    public Set<AffectedComponentEntity> createAffectedComponentEntities(final Set<ConfiguredComponent> affectedComponents, final RevisionManager revisionManager) {
+        return affectedComponents.stream()
+                .map(component -> {
+                    final AffectedComponentDTO affectedComponent = createAffectedComponentDto(component);
+                    final PermissionsDTO permissions = createPermissionsDto(component);
+                    final RevisionDTO revision = createRevisionDTO(revisionManager.getRevision(component.getIdentifier()));
+                    return entityFactory.createAffectedComponentEntity(affectedComponent, revision, permissions);
+                })
+                .collect(Collectors.toSet());
+    }
 
-    public VariableRegistryDTO createVariableRegistryDto(final ProcessGroup processGroup) {
+    public VariableRegistryDTO createVariableRegistryDto(final ProcessGroup processGroup, final RevisionManager revisionManager) {
         final ComponentVariableRegistry variableRegistry = processGroup.getVariableRegistry();
 
         final List<String> variableNames = variableRegistry.getVariableMap().keySet().stream()
@@ -2130,21 +2156,18 @@ public final class DtoFactory {
             variableDto.setValue(variableRegistry.getVariableValue(variableName));
             variableDto.setProcessGroupId(processGroup.getIdentifier());
 
-            final Set<ConfiguredComponent> affectedComponents = processGroup.getComponentsAffectedByVariable(variableName);
-            final Set<AffectedComponentDTO> affectedComponentDtos = affectedComponents.stream()
-                .map(component -> createAffectedComponentDto(component))
-                .collect(Collectors.toSet());
+            final Set<AffectedComponentEntity> affectedComponentEntities = createAffectedComponentEntities(processGroup.getComponentsAffectedByVariable(variableName), revisionManager);
 
             boolean canWrite = true;
-            for (final ConfiguredComponent component : affectedComponents) {
-                final PermissionsDTO permissions = createPermissionsDto(component);
+            for (final AffectedComponentEntity affectedComponent : affectedComponentEntities) {
+                final PermissionsDTO permissions = affectedComponent.getPermissions();
                 if (!permissions.getCanRead() || !permissions.getCanWrite()) {
                     canWrite = false;
                     break;
                 }
             }
 
-            variableDto.setAffectedComponents(affectedComponentDtos);
+            variableDto.setAffectedComponents(affectedComponentEntities);
 
             final VariableEntity variableEntity = new VariableEntity();
             variableEntity.setVariable(variableDto);
@@ -2178,6 +2201,8 @@ public final class DtoFactory {
         updateSteps.add(createVariableRegistryUpdateStepDto(request.getStartProcessorsStep()));
         dto.setUpdateSteps(updateSteps);
 
+        dto.setAffectedComponents(new HashSet<>(request.getAffectedComponents().values()));
+
         return dto;
     }
 
@@ -2190,42 +2215,41 @@ public final class DtoFactory {
     }
 
 
-    public VariableRegistryDTO populateAffectedComponents(final VariableRegistryDTO variableRegistry, final ProcessGroup group) {
+    public VariableRegistryDTO populateAffectedComponents(final VariableRegistryDTO variableRegistry, final ProcessGroup group, final RevisionManager revisionManager) {
         if (!group.getIdentifier().equals(variableRegistry.getProcessGroupId())) {
             throw new IllegalArgumentException("Variable Registry does not have the same Group ID as the given Process Group");
         }
 
         final Set<VariableEntity> variableEntities = new LinkedHashSet<>();
 
-        for (final VariableEntity inputEntity : variableRegistry.getVariables()) {
-            final VariableEntity entity = new VariableEntity();
+        if (variableRegistry.getVariables() != null) {
+            for (final VariableEntity inputEntity : variableRegistry.getVariables()) {
+                final VariableEntity entity = new VariableEntity();
 
-            final VariableDTO inputDto = inputEntity.getVariable();
-            final VariableDTO variableDto = new VariableDTO();
-            variableDto.setName(inputDto.getName());
-            variableDto.setValue(inputDto.getValue());
-            variableDto.setProcessGroupId(group.getIdentifier());
+                final VariableDTO inputDto = inputEntity.getVariable();
+                final VariableDTO variableDto = new VariableDTO();
+                variableDto.setName(inputDto.getName());
+                variableDto.setValue(inputDto.getValue());
+                variableDto.setProcessGroupId(group.getIdentifier());
 
-            final Set<ConfiguredComponent> affectedComponents = group.getComponentsAffectedByVariable(variableDto.getName());
-            final Set<AffectedComponentDTO> affectedComponentDtos = affectedComponents.stream()
-                .map(component -> createAffectedComponentDto(component))
-                .collect(Collectors.toSet());
+                final Set<AffectedComponentEntity> affectedComponentEntities = createAffectedComponentEntities(group.getComponentsAffectedByVariable(variableDto.getName()), revisionManager);
 
-            boolean canWrite = true;
-            for (final ConfiguredComponent component : affectedComponents) {
-                final PermissionsDTO permissions = createPermissionsDto(component);
-                if (!permissions.getCanRead() || !permissions.getCanWrite()) {
-                    canWrite = false;
-                    break;
+                boolean canWrite = true;
+                for (final AffectedComponentEntity affectedComponent : affectedComponentEntities) {
+                    final PermissionsDTO permissions = affectedComponent.getPermissions();
+                    if (!permissions.getCanRead() || !permissions.getCanWrite()) {
+                        canWrite = false;
+                        break;
+                    }
                 }
-            }
 
-            variableDto.setAffectedComponents(affectedComponentDtos);
+                variableDto.setAffectedComponents(affectedComponentEntities);
 
-            entity.setCanWrite(canWrite);
-            entity.setVariable(inputDto);
+                entity.setCanWrite(canWrite);
+                entity.setVariable(inputDto);
 
-            variableEntities.add(entity);
+                variableEntities.add(entity);
+            }
         }
 
         final VariableRegistryDTO registryDto = new VariableRegistryDTO();

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
index a7f370a..16781c6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/EntityFactory.java
@@ -33,6 +33,7 @@ import org.apache.nifi.web.api.dto.status.StatusHistoryDTO;
 import org.apache.nifi.web.api.entity.AccessPolicyEntity;
 import org.apache.nifi.web.api.entity.AccessPolicySummaryEntity;
 import org.apache.nifi.web.api.entity.ActionEntity;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.AllowableValueEntity;
 import org.apache.nifi.web.api.entity.BulletinEntity;
 import org.apache.nifi.web.api.entity.ComponentReferenceEntity;
@@ -311,6 +312,20 @@ public final class EntityFactory {
         return entity;
     }
 
+    public AffectedComponentEntity createAffectedComponentEntity(final AffectedComponentDTO dto, final RevisionDTO revision, final PermissionsDTO permissions) {
+        final AffectedComponentEntity entity = new AffectedComponentEntity();
+        entity.setRevision(revision);
+        if (dto != null) {
+            entity.setPermissions(permissions);
+            entity.setId(dto.getId());
+
+            if (permissions != null && permissions.getCanRead()) {
+                entity.setComponent(dto);
+            }
+        }
+        return entity;
+    }
+
     public UserGroupEntity createUserGroupEntity(final UserGroupDTO dto, final RevisionDTO revision, final PermissionsDTO permissions) {
         final UserGroupEntity entity = new UserGroupEntity();
         entity.setRevision(revision);

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessorDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessorDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessorDAO.java
index 1d88161..a1bf170 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessorDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/ProcessorDAO.java
@@ -59,9 +59,10 @@ public interface ProcessorDAO {
      * Gets all the Processor transfer objects for this controller.
      *
      * @param groupId group id
+     * @param includeDescendants if processors from descendant groups should be included
      * @return List of all the Processors
      */
-    Set<ProcessorNode> getProcessors(String groupId);
+    Set<ProcessorNode> getProcessors(String groupId, boolean includeDescendants);
 
     /**
      * Verifies the specified processor can be updated.

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessorDAO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessorDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessorDAO.java
index e11f9ad..429592c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessorDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardProcessorDAO.java
@@ -58,6 +58,7 @@ import java.util.Set;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
+import java.util.stream.Collectors;
 
 public class StandardProcessorDAO extends ComponentDAO implements ProcessorDAO {
 
@@ -307,9 +308,13 @@ public class StandardProcessorDAO extends ComponentDAO implements ProcessorDAO {
     }
 
     @Override
-    public Set<ProcessorNode> getProcessors(String groupId) {
+    public Set<ProcessorNode> getProcessors(String groupId, boolean includeDescendants) {
         ProcessGroup group = locateProcessGroup(flowController, groupId);
-        return group.getProcessors();
+        if (includeDescendants) {
+            return group.findAllProcessors().stream().collect(Collectors.toSet());
+        } else {
+            return group.getProcessors();
+        }
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
index 0d60df2..72cd5ed 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/pom.xml
@@ -461,6 +461,7 @@
                                                 <include>${staging.dir}/js/nf/canvas/nf-port-configuration.js</include>
                                                 <include>${staging.dir}/js/nf/canvas/nf-port-details.js</include>
                                                 <include>${staging.dir}/js/nf/canvas/nf-process-group-configuration.js</include>
+                                                <include>${staging.dir}/js/nf/canvas/nf-variable-registry.js</include>
                                                 <include>${staging.dir}/js/nf/canvas/nf-component-version.js</include>
                                                 <include>${staging.dir}/js/nf/canvas/nf-remote-process-group-configuration.js</include>
                                                 <include>${staging.dir}/js/nf/canvas/nf-remote-process-group-details.js</include>


[2/4] nifi git commit: NIFI-4280: - Adding support for the user to configure variables in the UI. - Updating the endpoints for changing variables as necessary. This closes #2135.

Posted by ma...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties
index a3ab7fc..9b04ece 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/resources/filters/canvas.properties
@@ -43,6 +43,7 @@ nf.canvas.script.tags=<script type="text/javascript" src="js/nf/nf-ng-bridge.js?
 <script type="text/javascript" src="js/nf/canvas/nf-port-configuration.js?${project.version}"></script>\n\
 <script type="text/javascript" src="js/nf/canvas/nf-port-details.js?${project.version}"></script>\n\
 <script type="text/javascript" src="js/nf/canvas/nf-process-group-configuration.js?${project.version}"></script>\n\
+<script type="text/javascript" src="js/nf/canvas/nf-variable-registry.js?${project.version}"></script>\n\
 <script type="text/javascript" src="js/nf/canvas/nf-component-version.js?${project.version}"></script>\n\
 <script type="text/javascript" src="js/nf/canvas/nf-remote-process-group-configuration.js?${project.version}"></script>\n\
 <script type="text/javascript" src="js/nf/canvas/nf-remote-process-group-details.js?${project.version}"></script>\n\

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
index 1928f7a..3c7d407 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp
@@ -129,6 +129,7 @@
         <jsp:include page="/WEB-INF/partials/canvas/reporting-task-configuration.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/processor-configuration.jsp"/>
         <jsp:include page="/WEB-INF/partials/processor-details.jsp"/>
+        <jsp:include page="/WEB-INF/partials/canvas/variable-configuration.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/process-group-configuration.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/override-policy-dialog.jsp"/>
         <jsp:include page="/WEB-INF/partials/canvas/policy-management.jsp"/>

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/variable-configuration.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/variable-configuration.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/variable-configuration.jsp
new file mode 100644
index 0000000..f26c2eb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/variable-configuration.jsp
@@ -0,0 +1,93 @@
+<%--
+ 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.
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<div id="variable-registry-dialog" class="hidden">
+    <div class="dialog-content">
+        <div class="settings-left">
+            <div class="setting">
+                <div style="float: left;">
+                    <div class="setting-name">Process Group</div>
+                    <div class="setting-field">
+                        <span id="process-group-variable-registry"></span>
+                        <span id="variable-registry-process-group-id" class="hidden"></span>
+                    </div>
+                </div>
+                <div id="add-variable"><button class="button fa fa-plus"></button></div>
+                <div class="clear"></div>
+            </div>
+            <div id="variable-registry-table"></div>
+            <div id="variable-update-status" class="hidden">
+                <div class="setting">
+                    <div class="setting-name">
+                        Steps to update variables
+                    </div>
+                    <div class="setting-field">
+                        <ol id="variable-update-steps"></ol>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="spacer">&nbsp;</div>
+        <div class="settings-right">
+            <div class="setting">
+                <div class="setting-name">
+                    Variables
+                </div>
+                <div class="setting-field">
+                    <div id="affected-components-context"></div>
+                </div>
+            </div>
+            <div class="setting">
+                <div class="setting-name">
+                    Referencing Processors
+                    <div class="fa fa-question-circle" alt="Info" title="Processors referencing this variable."></div>
+                </div>
+                <div class="setting-field">
+                    <ul id="variable-registry-affected-processors"></ul>
+                </div>
+            </div>
+            <div class="setting">
+                <div class="setting-name">
+                    Referencing Controller Services
+                    <div class="fa fa-question-circle" alt="Info" title="Controller Services referencing this variable."></div>
+                </div>
+                <div class="setting-field">
+                    <ul id="variable-registry-affected-controller-services"></ul>
+                </div>
+            </div>
+            <div class="setting">
+                <div class="setting-name">
+                    Unauthorized referencing components
+                    <div class="fa fa-question-circle" alt="Info" title="Referencing components for which READ or WRITE permissions are not granted."></div>
+                </div>
+                <div class="setting-field">
+                    <ul id="variable-registry-affected-unauthorized-components"></ul>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<div id="new-variable-dialog" class="dialog cancellable small-dialog hidden">
+    <div class="dialog-content">
+        <div>
+            <div class="setting-name">Variable name</div>
+            <div class="setting-field new-variable-name-container">
+                <input id="new-variable-name" type="text"/>
+            </div>
+        </div>
+    </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/controller-service.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/controller-service.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/controller-service.css
index 49b7376..fe5808e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/controller-service.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/controller-service.css
@@ -79,6 +79,10 @@ ul.referencing-component-listing li {
     white-space: nowrap;
 }
 
+div.referencing-component-state {
+    width: 13px;
+}
+
 div.referencing-component-state.disabled:before {
     content: '\e802';
     font-family: flowfont;

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
index 1e01919..cb5282b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/dialog.css
@@ -213,6 +213,45 @@ div.progress-label {
 }
 
 /*
+    Variable Registry
+ */
+
+#variable-registry-dialog {
+    width: 850px;
+    height: 575px;
+}
+
+#variable-registry-dialog div.settings-left {
+    float: left;
+    width: 65%;
+}
+
+#variable-registry-dialog div.settings-right {
+    float: left;
+    width: 33%;
+}
+
+#variable-registry-table {
+    height: 400px;
+}
+
+#add-variable {
+    float: right;
+    margin-bottom: 4px;
+    font-size: 16px;
+    text-transform: uppercase;
+}
+
+li.affected-component-container {
+    margin-bottom: 3px;
+    height: 16px;
+}
+
+div.slick-cell div.overridden {
+    text-decoration: line-through;
+}
+
+/*
     General dialog styles.
 */
 
@@ -239,3 +278,15 @@ ul.result li {
     float: left;
     width: 2%;
 }
+
+div.variable-step {
+    width: 16px;
+    height: 16px;
+    background-color: transparent;
+    float: right;
+}
+
+#variable-update-steps li {
+    width: 300px;
+    margin-bottom: 2px;
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/propertytable/jquery.propertytable.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/propertytable/jquery.propertytable.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/propertytable/jquery.propertytable.js
index d1006ab..abd27b2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/propertytable/jquery.propertytable.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/jquery/propertytable/jquery.propertytable.js
@@ -1682,14 +1682,14 @@
                         // build the new property dialog
                         var newPropertyDialogMarkup =
                             '<div id="new-property-dialog" class="dialog cancellable small-dialog hidden">' +
-                            '<div class="dialog-content">' +
-                            '<div>' +
-                            '<div class="setting-name">Property name</div>' +
-                            '<div class="setting-field new-property-name-container">' +
-                            '<input class="new-property-name" type="text"/>' +
-                            '</div>' +
-                            '</div>' +
-                            '</div>' +
+                                '<div class="dialog-content">' +
+                                    '<div>' +
+                                    '<div class="setting-name">Property name</div>' +
+                                        '<div class="setting-field new-property-name-container">' +
+                                            '<input class="new-property-name" type="text"/>' +
+                                        '</div>' +
+                                    '</div>' +
+                                '</div>' +
                             '</div>';
 
                         var newPropertyDialog = $(newPropertyDialogMarkup).appendTo(options.dialogContainer);

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
index 7050df0..90a1140 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js
@@ -31,6 +31,7 @@
                 'nf.GoTo',
                 'nf.ng.Bridge',
                 'nf.Shell',
+                'nf.VariableRegistry',
                 'nf.ComponentState',
                 'nf.Draggable',
                 'nf.Birdseye',
@@ -54,8 +55,8 @@
                 'nf.ComponentVersion',
                 'nf.QueueListing',
                 'nf.StatusHistory'],
-            function ($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfComponentState, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory) {
-                return (nf.Actions = factory($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfComponentState, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory));
+            function ($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfVariableRegistry, nfComponentState, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory) {
+                return (nf.Actions = factory($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfVariableRegistry, nfComponentState, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory));
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = (nf.Actions =
@@ -71,6 +72,7 @@
                 require('nf.GoTo'),
                 require('nf.ng.Bridge'),
                 require('nf.Shell'),
+                require('nf.VariableRegistry'),
                 require('nf.ComponentState'),
                 require('nf.Draggable'),
                 require('nf.Birdseye'),
@@ -107,6 +109,7 @@
             root.nf.GoTo,
             root.nf.ng.Bridge,
             root.nf.Shell,
+            root.nf.VariableRegistry,
             root.nf.ComponentState,
             root.nf.Draggable,
             root.nf.Birdseye,
@@ -131,7 +134,7 @@
             root.nf.QueueListing,
             root.nf.StatusHistory);
     }
-}(this, function ($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfComponentState, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory) {
+}(this, function ($, d3, nfCanvasUtils, nfCommon, nfDialog, nfClient, nfErrorHandler, nfClipboard, nfSnippet, nfGoto, nfNgBridge, nfShell, nfVariableRegistry, nfComponentState, nfDraggable, nfBirdseye, nfConnection, nfGraph, nfProcessGroupConfiguration, nfProcessorConfiguration, nfProcessorDetails, nfLabelConfiguration, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupDetails, nfPortConfiguration, nfPortDetails, nfConnectionConfiguration, nfConnectionDetails, nfPolicyManagement, nfRemoteProcessGroup, nfLabel, nfProcessor, nfRemoteProcessGroupPorts, nfComponentVersion, nfQueueListing, nfStatusHistory) {
     'use strict';
 
     var config = {
@@ -1234,6 +1237,22 @@
         },
 
         /**
+         * Opens the variable registry for the specified selection of the current group if the selection is emtpy.
+         *
+         * @param {selection} selection
+         */
+        openVariableRegistry: function (selection) {
+            if (selection.empty()) {
+                nfVariableRegistry.showVariables(nfCanvasUtils.getGroupId());
+            } else if (selection.size() === 1) {
+                var selectionData = selection.datum();
+                if (nfCanvasUtils.isProcessGroup(selection)) {
+                    nfVariableRegistry.showVariables(selectionData.id);
+                }
+            }
+        },
+
+        /**
          * Views the state for the specified processor.
          *
          * @param {selection} selection

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
index 77ef67a..e3ab5c9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js
@@ -37,6 +37,7 @@
                 'nf.Snippet',
                 'nf.Actions',
                 'nf.QueueListing',
+                'nf.VariableRegistry',
                 'nf.ComponentState',
                 'nf.ComponentVersion',
                 'nf.Draggable',
@@ -81,8 +82,8 @@
                 'nf.ng.Canvas.OperateCtrl',
                 'nf.ng.BreadcrumbsDirective',
                 'nf.ng.DraggableDirective'],
-            function ($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfComponentState, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, labelComponent, graphControlsCtrl, nav
 igateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective) {
-                return factory($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfComponentState, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, labelComponent, graphControls
 Ctrl, navigateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective);
+            function ($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfVariableRegistry, nfComponentState, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, labelComponent, gr
 aphControlsCtrl, navigateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective) {
+                return factory($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfVariableRegistry, nfComponentState, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, labelComp
 onent, graphControlsCtrl, navigateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective);
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = factory(require('jquery'),
@@ -103,6 +104,7 @@
             require('nf.Actions'),
             require('nf.Snippet'),
             require('nf.QueueListing'),
+            require('nf.VariableRegistry'),
             require('nf.ComponentState'),
             require('nf.ComponentVersion'),
             require('nf.Draggable'),
@@ -166,6 +168,7 @@
             root.nf.Actions,
             root.nf.Snippet,
             root.nf.QueueListing,
+            root.nf.VariableRegistry,
             root.nf.ComponentState,
             root.nf.ComponentVersion,
             root.nf.Draggable,
@@ -211,7 +214,7 @@
             root.nf.ng.BreadcrumbsDirective,
             root.nf.ng.DraggableDirective);
     }
-}(this, function ($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfComponentState, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, labelComponent, graphControlsCtrl, navigat
 eCtrl, operateCtrl, breadcrumbsDirective, draggableDirective) {
+}(this, function ($, angular, nfCommon, nfCanvasUtils, nfErrorHandler, nfClient, nfClusterSummary, nfDialog, nfStorage, nfCanvas, nfGraph, nfContextMenu, nfQuickSelect, nfShell, nfSettings, nfActions, nfSnippet, nfQueueListing, nfVariableRegistry, nfComponentState, nfComponentVersion, nfDraggable, nfConnectable, nfStatusHistory, nfBirdseye, nfConnectionConfiguration, nfControllerService, nfReportingTask, nfPolicyManagement, nfProcessorConfiguration, nfProcessGroupConfiguration, nfControllerServices, nfRemoteProcessGroupConfiguration, nfRemoteProcessGroupPorts, nfPortConfiguration, nfLabelConfiguration, nfProcessorDetails, nfPortDetails, nfConnectionDetails, nfRemoteProcessGroupDetails, nfGoto, nfNgBridge, appCtrl, appConfig, serviceProvider, breadcrumbsCtrl, headerCtrl, flowStatusCtrl, globalMenuCtrl, toolboxCtrl, processorComponent, inputPortComponent, outputPortComponent, processGroupComponent, remoteProcessGroupComponent, funnelComponent, templateComponent, labelComponent, graphC
 ontrolsCtrl, navigateCtrl, operateCtrl, breadcrumbsDirective, draggableDirective) {
 
     var config = {
         urls: {
@@ -347,6 +350,7 @@
                     nfSettings.init();
                     nfActions.init();
                     nfQueueListing.init();
+                    nfVariableRegistry.init();
                     nfComponentState.init();
                     nfComponentVersion.init(nfSettings);
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
index 54f1d14..4ead97b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js
@@ -239,6 +239,25 @@
         },
 
         /**
+         * Queries for bulletins for the specified components.
+         *
+         * @param {array} componentIds
+         * @returns {deferred}
+         */
+        queryBulletins: function (componentIds) {
+            var ids = componentIds.join('|');
+
+            return $.ajax({
+                type: 'GET',
+                url: '../nifi-api/flow/bulletin-board',
+                data: {
+                    sourceId: ids
+                },
+                dataType: 'json'
+            }).fail(nfErrorHandler.handleAjaxError);
+        },
+
+        /**
          * Shows the specified component in the specified group.
          *
          * @param {string} groupId       The id of the group
@@ -282,6 +301,8 @@
                         });
                     }
                 });
+
+                return refreshGraph;
             }
         },
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
index 65346b2..0ebcf6a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js
@@ -65,6 +65,15 @@
     };
 
     /**
+     * Determines whether the component in the specified selection has variables.
+     *
+     * @param {selection} selection         The selection of currently selected components
+     */
+    var hasVariables = function (selection) {
+        return selection.empty() || nfCanvasUtils.isProcessGroup(selection);
+    };
+
+    /**
      * Determines whether the component in the specified selection has configuration details.
      *
      * @param {selection} selection         The selection of currently selected components
@@ -537,6 +546,7 @@
         {separator: true},
         {id: 'show-configuration-menu-item', condition: isConfigurable, menuItem: {clazz: 'fa fa-gear', text: 'Configure', action: 'showConfiguration'}},
         {id: 'show-details-menu-item', condition: hasDetails, menuItem: {clazz: 'fa fa-gear', text: 'View configuration', action: 'showDetails'}},
+        {id: 'variable-registry-menu-item', condition: hasVariables, menuItem: {clazz: 'fa', text: 'Variables', action: 'openVariableRegistry'}},
         {separator: true},
         {id: 'enter-group-menu-item', condition: isProcessGroup, menuItem: {clazz: 'fa fa-sign-in', text: 'Enter group', action: 'enterGroup'}},
         {separator: true},

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-controller-service.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-service.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-controller-service.js
index 90324bf..c7d9c43 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-controller-service.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-controller-service.js
@@ -586,7 +586,7 @@
         });
 
         // query for the bulletins
-        queryBulletins(referencingComponentIds).done(function (response) {
+        nfCanvasUtils.queryBulletins(referencingComponentIds).done(function (response) {
             var bulletins = response.bulletinBoard.bulletins;
             updateReferencingComponentBulletins(bulletins);
         });
@@ -623,25 +623,6 @@
     };
 
     /**
-     * Queries for bulletins for the specified components.
-     *
-     * @param {array} componentIds
-     * @returns {deferred}
-     */
-    var queryBulletins = function (componentIds) {
-        var ids = componentIds.join('|');
-
-        return $.ajax({
-            type: 'GET',
-            url: '../nifi-api/flow/bulletin-board',
-            data: {
-                sourceId: ids
-            },
-            dataType: 'json'
-        }).fail(nfErrorHandler.handleAjaxError);
-    };
-
-    /**
      * Sets whether the specified controller service is enabled.
      *
      * @param {jQuery} serviceTable
@@ -688,7 +669,7 @@
                         return service.state === 'DISABLED';
                     }
                 }, function (service) {
-                    return queryBulletins([service.id]);
+                    return nfCanvasUtils.queryBulletins([service.id]);
                 }, pollCondition);
 
                 // once the service has updated, resolve and render the updated service
@@ -961,7 +942,7 @@
                 }
             });
 
-            return queryBulletins(referencingSchedulableComponents);
+            return nfCanvasUtils.queryBulletins(referencingSchedulableComponents);
         }, pollCondition);
     };
 
@@ -1006,7 +987,7 @@
                 }
             });
 
-            return queryBulletins(referencingSchedulableComponents);
+            return nfCanvasUtils.queryBulletins(referencingSchedulableComponents);
         }, pollCondition);
     };
 
@@ -1051,7 +1032,7 @@
                 }
             });
 
-            return queryBulletins(referencingSchedulableComponents);
+            return nfCanvasUtils.queryBulletins(referencingSchedulableComponents);
         }, pollCondition);
     };
 
@@ -1164,7 +1145,7 @@
         $('#disable-controller-service-dialog').modal('setButtonModel', buttons).modal('show');
 
         // load the bulletins
-        queryBulletins([controllerService.id]).done(function (response) {
+        nfCanvasUtils.queryBulletins([controllerService.id]).done(function (response) {
             updateBulletins(response.bulletinBoard.bulletins, $('#disable-controller-service-bulletins'));
         });
 
@@ -1216,7 +1197,7 @@
         $('#enable-controller-service-dialog').modal('setButtonModel', buttons).modal('show');
 
         // load the bulletins
-        queryBulletins([controllerService.id]).done(function (response) {
+        nfCanvasUtils.queryBulletins([controllerService.id]).done(function (response) {
             updateBulletins(response.bulletinBoard.bulletins, $('#enable-controller-service-bulletins'));
         });
 


[4/4] nifi git commit: NIFI-4280: - Adding support for the user to configure variables in the UI. - Updating the endpoints for changing variables as necessary. This closes #2135.

Posted by ma...@apache.org.
NIFI-4280:
- Adding support for the user to configure variables in the UI.
- Updating the endpoints for changing variables as necessary.
This closes #2135.


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/eac47e90
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/eac47e90
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/eac47e90

Branch: refs/heads/master
Commit: eac47e90cbfa39a8e40512379b02ff745262b5af
Parents: 9138326
Author: Matt Gilman <ma...@gmail.com>
Authored: Thu Aug 17 16:51:23 2017 -0400
Committer: Mark Payne <ma...@hotmail.com>
Committed: Thu Sep 14 11:12:54 2017 -0400

----------------------------------------------------------------------
 .../nifi/web/api/dto/AffectedComponentDTO.java  |   92 +-
 ...ontrollerServiceReferencingComponentDTO.java |    8 +-
 .../apache/nifi/web/api/dto/VariableDTO.java    |   13 +-
 .../nifi/web/api/dto/VariableRegistryDTO.java   |   16 +-
 .../dto/VariableRegistryUpdateRequestDTO.java   |   27 +-
 .../web/api/entity/AffectedComponentEntity.java |   44 +
 .../web/api/entity/VariableRegistryEntity.java  |   13 +-
 .../VariableRegistryUpdateRequestEntity.java    |   15 +-
 .../http/StandardHttpResponseMapper.java        |   21 +-
 .../VariableRegistryEndpointMerger.java         |  113 ++
 .../manager/AffectedComponentEntityMerger.java  |  102 ++
 .../nifi/groups/StandardProcessGroup.java       |   58 +-
 .../variable/VariableRegistryUpdateRequest.java |   32 +-
 .../org/apache/nifi/web/NiFiServiceFacade.java  |   31 +-
 .../nifi/web/StandardNiFiServiceFacade.java     |   38 +-
 .../nifi/web/api/ProcessGroupResource.java      |  690 +++++---
 .../org/apache/nifi/web/api/dto/DtoFactory.java |  148 +-
 .../apache/nifi/web/api/dto/EntityFactory.java  |   15 +
 .../org/apache/nifi/web/dao/ProcessorDAO.java   |    3 +-
 .../nifi/web/dao/impl/StandardProcessorDAO.java |    9 +-
 .../nifi-framework/nifi-web/nifi-web-ui/pom.xml |    1 +
 .../main/resources/filters/canvas.properties    |    1 +
 .../src/main/webapp/WEB-INF/pages/canvas.jsp    |    1 +
 .../partials/canvas/variable-configuration.jsp  |   93 +
 .../src/main/webapp/css/controller-service.css  |    4 +
 .../nifi-web-ui/src/main/webapp/css/dialog.css  |   51 +
 .../propertytable/jquery.propertytable.js       |   16 +-
 .../src/main/webapp/js/nf/canvas/nf-actions.js  |   25 +-
 .../webapp/js/nf/canvas/nf-canvas-bootstrap.js  |   10 +-
 .../main/webapp/js/nf/canvas/nf-canvas-utils.js |   21 +
 .../main/webapp/js/nf/canvas/nf-context-menu.js |   10 +
 .../js/nf/canvas/nf-controller-service.js       |   33 +-
 .../webapp/js/nf/canvas/nf-variable-registry.js | 1633 ++++++++++++++++++
 33 files changed, 2897 insertions(+), 490 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
index 5d631ed..28999a5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/AffectedComponentDTO.java
@@ -17,43 +17,101 @@
 
 package org.apache.nifi.web.api.dto;
 
-import javax.xml.bind.annotation.XmlType;
-
 import com.wordnik.swagger.annotations.ApiModelProperty;
 
+import javax.xml.bind.annotation.XmlType;
+import java.util.Collection;
+
 @XmlType(name = "affectedComponent")
 public class AffectedComponentDTO {
     public static final String COMPONENT_TYPE_PROCESSOR = "PROCESSOR";
     public static final String COMPONENT_TYPE_CONTROLLER_SERVICE = "CONTROLLER_SERVICE";
 
-    private String parentGroupId;
-    private String componentId;
-    private String componentType;
+    private String processGroupId;
+    private String id;
+    private String referenceType;
+    private String name;
+    private String state;
+    private Integer activeThreadCount;
+
+    private Collection<String> validationErrors;
 
     @ApiModelProperty("The UUID of the Process Group that this component is in")
-    public String getParentGroupId() {
-        return parentGroupId;
+    public String getProcessGroupId() {
+        return processGroupId;
     }
 
-    public void setParentGroupId(final String parentGroupId) {
-        this.parentGroupId = parentGroupId;
+    public void setProcessGroupId(final String processGroupId) {
+        this.processGroupId = processGroupId;
     }
 
     @ApiModelProperty("The UUID of this component")
-    public String getComponentId() {
-        return componentId;
+    public String getId() {
+        return id;
     }
 
-    public void setComponentId(final String componentId) {
-        this.componentId = componentId;
+    public void setId(final String id) {
+        this.id = id;
     }
 
     @ApiModelProperty(value = "The type of this component", allowableValues = COMPONENT_TYPE_PROCESSOR + "," + COMPONENT_TYPE_CONTROLLER_SERVICE)
-    public String getComponentType() {
-        return componentType;
+    public String getReferenceType() {
+        return referenceType;
+    }
+
+    public void setReferenceType(final String referenceType) {
+        this.referenceType = referenceType;
+    }
+
+    @ApiModelProperty("The name of this component.")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * @return scheduled state of the processor referencing a controller service. If this component is another service, this field represents the controller service state
+     */
+    @ApiModelProperty(
+            value = "The scheduled state of a processor or reporting task referencing a controller service. If this component is another controller "
+                    + "service, this field represents the controller service state."
+    )
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+
+    /**
+     * @return active thread count for the referencing component
+     */
+    @ApiModelProperty(
+            value = "The number of active threads for the referencing component."
+    )
+    public Integer getActiveThreadCount() {
+        return activeThreadCount;
+    }
+
+    public void setActiveThreadCount(Integer activeThreadCount) {
+        this.activeThreadCount = activeThreadCount;
+    }
+
+    /**
+     * @return Any validation error associated with this component
+     */
+    @ApiModelProperty(
+            value = "The validation errors for the component."
+    )
+    public Collection<String> getValidationErrors() {
+        return validationErrors;
     }
 
-    public void setComponentType(final String componentType) {
-        this.componentType = componentType;
+    public void setValidationErrors(Collection<String> validationErrors) {
+        this.validationErrors = validationErrors;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerServiceReferencingComponentDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerServiceReferencingComponentDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerServiceReferencingComponentDTO.java
index 380e6ce..6279ac3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerServiceReferencingComponentDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ControllerServiceReferencingComponentDTO.java
@@ -19,10 +19,10 @@ package org.apache.nifi.web.api.dto;
 import com.wordnik.swagger.annotations.ApiModelProperty;
 import org.apache.nifi.web.api.entity.ControllerServiceReferencingComponentEntity;
 
+import javax.xml.bind.annotation.XmlType;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
-import javax.xml.bind.annotation.XmlType;
 
 /**
  * A component referencing a controller service. This can either be another controller service or a processor. Depending on the type of component different properties may be set.
@@ -105,11 +105,11 @@ public class ControllerServiceReferencingComponentDTO {
     }
 
     /**
-     * @return state of the processor referencing a controller service. If this component is another service, this field is blank
+     * @return scheduled state of the processor referencing a controller service. If this component is another service, this field represents the controller service state
      */
     @ApiModelProperty(
-            value = "The state of a processor or reporting task referencing a controller service. If this component is another controller "
-                    + "service, this field is blank."
+            value = "The scheduled state of a processor or reporting task referencing a controller service. If this component is another controller "
+                    + "service, this field represents the controller service state."
     )
     public String getState() {
         return state;

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableDTO.java
index c686316..89ea27a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableDTO.java
@@ -17,19 +17,18 @@
 
 package org.apache.nifi.web.api.dto;
 
-import java.util.HashSet;
-import java.util.Set;
+import com.wordnik.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 
 import javax.xml.bind.annotation.XmlType;
-
-import com.wordnik.swagger.annotations.ApiModelProperty;
+import java.util.Set;
 
 @XmlType(name = "variable")
 public class VariableDTO {
     private String name;
     private String value;
     private String processGroupId;
-    private Set<AffectedComponentDTO> affectedComponents = new HashSet<>();
+    private Set<AffectedComponentEntity> affectedComponents;
 
     @ApiModelProperty("The name of the variable")
     public String getName() {
@@ -59,11 +58,11 @@ public class VariableDTO {
     }
 
     @ApiModelProperty(value = "A set of all components that will be affected if the value of this variable is changed", readOnly = true)
-    public Set<AffectedComponentDTO> getAffectedComponents() {
+    public Set<AffectedComponentEntity> getAffectedComponents() {
         return affectedComponents;
     }
 
-    public void setAffectedComponents(Set<AffectedComponentDTO> affectedComponents) {
+    public void setAffectedComponents(Set<AffectedComponentEntity> affectedComponents) {
         this.affectedComponents = affectedComponents;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableRegistryDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableRegistryDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableRegistryDTO.java
index c106a9a..8f37532 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableRegistryDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableRegistryDTO.java
@@ -17,18 +17,16 @@
 
 package org.apache.nifi.web.api.dto;
 
-import java.util.Set;
-
-import javax.xml.bind.annotation.XmlType;
-
+import com.wordnik.swagger.annotations.ApiModelProperty;
 import org.apache.nifi.web.api.entity.VariableEntity;
 
-import com.wordnik.swagger.annotations.ApiModelProperty;
+import javax.xml.bind.annotation.XmlType;
+import java.util.Set;
 
 @XmlType(name = "variableRegistry")
 public class VariableRegistryDTO {
     private Set<VariableEntity> variables;
-    private String groupId;
+    private String processGroupId;
 
     public void setVariables(final Set<VariableEntity> variables) {
         this.variables = variables;
@@ -39,12 +37,12 @@ public class VariableRegistryDTO {
         return variables;
     }
 
-    public void setProcessGroupId(final String groupId) {
-        this.groupId = groupId;
+    public void setProcessGroupId(final String processGroupId) {
+        this.processGroupId = processGroupId;
     }
 
     @ApiModelProperty("The UUID of the Process Group that this Variable Registry belongs to")
     public String getProcessGroupId() {
-        return groupId;
+        return processGroupId;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableRegistryUpdateRequestDTO.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableRegistryUpdateRequestDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableRegistryUpdateRequestDTO.java
index 06a0dc2..e57e30a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableRegistryUpdateRequestDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/VariableRegistryUpdateRequestDTO.java
@@ -17,27 +17,27 @@
 
 package org.apache.nifi.web.api.dto;
 
-import java.util.Date;
-import java.util.List;
+import com.wordnik.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.web.api.dto.util.TimestampAdapter;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 
 import javax.xml.bind.annotation.XmlType;
 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
-
-import org.apache.nifi.web.api.dto.util.TimestampAdapter;
-
-import com.wordnik.swagger.annotations.ApiModelProperty;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
 
 @XmlType(name = "variableRegistryUpdateRequest")
 public class VariableRegistryUpdateRequestDTO {
     private String requestId;
     private String processGroupId;
     private String uri;
-    private Date submissionTime = new Date();
-    private Date lastUpdated = new Date();
+    private Date submissionTime;
+    private Date lastUpdated;
     private boolean complete = false;
     private String failureReason;
     private List<VariableRegistryUpdateStepDTO> updateSteps;
-
+    private Set<AffectedComponentEntity> affectedComponents;
 
     @ApiModelProperty("The unique ID of the Process Group that the variable registry belongs to")
     public String getProcessGroupId() {
@@ -112,4 +112,13 @@ public class VariableRegistryUpdateRequestDTO {
     public void setFailureReason(String reason) {
         this.failureReason = reason;
     }
+
+    @ApiModelProperty(value = "A set of all components that will be affected if the value of this variable is changed", readOnly = true)
+    public Set<AffectedComponentEntity> getAffectedComponents() {
+        return affectedComponents;
+    }
+
+    public void setAffectedComponents(Set<AffectedComponentEntity> affectedComponents) {
+        this.affectedComponents = affectedComponents;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AffectedComponentEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AffectedComponentEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AffectedComponentEntity.java
new file mode 100644
index 0000000..0f28f73
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/AffectedComponentEntity.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+
+package org.apache.nifi.web.api.entity;
+
+import org.apache.nifi.web.api.dto.AffectedComponentDTO;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * A serialized representation of this class can be placed in the entity body of a response to the API.
+ * This particular entity holds a reference to component that references a variable.
+ */
+@XmlRootElement(name = "affectComponentEntity")
+public class AffectedComponentEntity extends ComponentEntity implements Permissible<AffectedComponentDTO> {
+
+    private AffectedComponentDTO component;
+
+    /**
+     * @return variable referencing components that is being serialized
+     */
+    public AffectedComponentDTO getComponent() {
+        return component;
+    }
+
+    public void setComponent(AffectedComponentDTO component) {
+        this.component = component;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VariableRegistryEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VariableRegistryEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VariableRegistryEntity.java
index d876453..1b7da0c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VariableRegistryEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VariableRegistryEntity.java
@@ -17,16 +17,15 @@
 
 package org.apache.nifi.web.api.entity;
 
-import javax.xml.bind.annotation.XmlRootElement;
-
+import com.wordnik.swagger.annotations.ApiModelProperty;
 import org.apache.nifi.web.api.dto.RevisionDTO;
 import org.apache.nifi.web.api.dto.VariableRegistryDTO;
 
-import com.wordnik.swagger.annotations.ApiModelProperty;
+import javax.xml.bind.annotation.XmlRootElement;
 
 @XmlRootElement(name = "variableRegistryEntity")
 public class VariableRegistryEntity extends Entity {
-    private RevisionDTO groupRevision;
+    private RevisionDTO processGroupRevision;
     private VariableRegistryDTO variableRegistry;
 
 
@@ -41,10 +40,10 @@ public class VariableRegistryEntity extends Entity {
 
     @ApiModelProperty("The revision of the Process Group that the Variable Registry belongs to")
     public RevisionDTO getProcessGroupRevision() {
-        return groupRevision;
+        return processGroupRevision;
     }
 
-    public void setProcessGroupRevision(RevisionDTO revision) {
-        this.groupRevision = revision;
+    public void setProcessGroupRevision(RevisionDTO processGroupRevision) {
+        this.processGroupRevision = processGroupRevision;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VariableRegistryUpdateRequestEntity.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VariableRegistryUpdateRequestEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VariableRegistryUpdateRequestEntity.java
index 77257af..bfeb9ce 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VariableRegistryUpdateRequestEntity.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/VariableRegistryUpdateRequestEntity.java
@@ -17,16 +17,15 @@
 
 package org.apache.nifi.web.api.entity;
 
-import javax.xml.bind.annotation.XmlRootElement;
-
+import com.wordnik.swagger.annotations.ApiModelProperty;
 import org.apache.nifi.web.api.dto.RevisionDTO;
 import org.apache.nifi.web.api.dto.VariableRegistryUpdateRequestDTO;
 
-import com.wordnik.swagger.annotations.ApiModelProperty;
+import javax.xml.bind.annotation.XmlRootElement;
 
 @XmlRootElement(name = "variableRegistryUpdateRequestEntity")
 public class VariableRegistryUpdateRequestEntity extends Entity {
-    private VariableRegistryUpdateRequestDTO requestDto;
+    private VariableRegistryUpdateRequestDTO request;
     private RevisionDTO processGroupRevision;
 
     @ApiModelProperty("The revision for the Process Group that owns this variable registry.")
@@ -39,11 +38,11 @@ public class VariableRegistryUpdateRequestEntity extends Entity {
     }
 
     @ApiModelProperty("The Variable Registry Update Request")
-    public VariableRegistryUpdateRequestDTO getRequestDto() {
-        return requestDto;
+    public VariableRegistryUpdateRequestDTO getRequest() {
+        return request;
     }
 
-    public void setRequestDto(VariableRegistryUpdateRequestDTO requestDto) {
-        this.requestDto = requestDto;
+    public void setRequest(VariableRegistryUpdateRequestDTO request) {
+        this.request = request;
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
index c102746..fa23603 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/StandardHttpResponseMapper.java
@@ -16,16 +16,6 @@
  */
 package org.apache.nifi.cluster.coordination.http;
 
-import java.io.IOException;
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-import javax.ws.rs.core.StreamingOutput;
-
 import org.apache.nifi.cluster.coordination.http.endpoints.AccessPolicyEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.BulletinBoardEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.ComponentStateEndpointMerger;
@@ -79,6 +69,7 @@ import org.apache.nifi.cluster.coordination.http.endpoints.UserEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.UserGroupEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.UserGroupsEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.endpoints.UsersEndpointMerger;
+import org.apache.nifi.cluster.coordination.http.endpoints.VariableRegistryEndpointMerger;
 import org.apache.nifi.cluster.coordination.http.replication.RequestReplicator;
 import org.apache.nifi.cluster.manager.NodeResponse;
 import org.apache.nifi.stream.io.NullOutputStream;
@@ -87,6 +78,15 @@ import org.apache.nifi.util.NiFiProperties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.ws.rs.core.StreamingOutput;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
 public class StandardHttpResponseMapper implements HttpResponseMapper {
 
     private Logger logger = LoggerFactory.getLogger(StandardHttpResponseMapper.class);
@@ -154,6 +154,7 @@ public class StandardHttpResponseMapper implements HttpResponseMapper {
         endpointMergers.add(new UserGroupEndpointMerger());
         endpointMergers.add(new AccessPolicyEndpointMerger());
         endpointMergers.add(new SearchUsersEndpointMerger());
+        endpointMergers.add(new VariableRegistryEndpointMerger());
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VariableRegistryEndpointMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VariableRegistryEndpointMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VariableRegistryEndpointMerger.java
new file mode 100644
index 0000000..0420911
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/endpoints/VariableRegistryEndpointMerger.java
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+package org.apache.nifi.cluster.coordination.http.endpoints;
+
+import org.apache.nifi.cluster.coordination.http.EndpointResponseMerger;
+import org.apache.nifi.cluster.manager.AffectedComponentEntityMerger;
+import org.apache.nifi.cluster.manager.NodeResponse;
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.web.api.dto.VariableDTO;
+import org.apache.nifi.web.api.dto.VariableRegistryDTO;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.VariableEntity;
+import org.apache.nifi.web.api.entity.VariableRegistryEntity;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class VariableRegistryEndpointMerger extends AbstractSingleEntityEndpoint<VariableRegistryEntity> implements EndpointResponseMerger {
+    public static final Pattern VARIABLE_REGISTRY_UPDATE_REQUEST_URI_PATTERN = Pattern.compile("/nifi-api/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/variable-registry");
+
+    private final AffectedComponentEntityMerger affectedComponentEntityMerger = new AffectedComponentEntityMerger();
+
+    @Override
+    public boolean canHandle(final URI uri, final String method) {
+        if (("GET".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) && (VARIABLE_REGISTRY_UPDATE_REQUEST_URI_PATTERN.matcher(uri.getPath()).matches())) {
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    protected Class<VariableRegistryEntity> getEntityClass() {
+        return VariableRegistryEntity.class;
+    }
+
+    @Override
+    protected void mergeResponses(final VariableRegistryEntity clientEntity, final Map<NodeIdentifier, VariableRegistryEntity> entityMap,
+                                  final Set<NodeResponse> successfulResponses, final Set<NodeResponse> problematicResponses) {
+
+        final VariableRegistryDTO clientVariableRegistry = clientEntity.getVariableRegistry();
+        final Set<VariableEntity> clientVariableEntities = clientVariableRegistry.getVariables();
+
+        if (clientVariableEntities != null) {
+            for (final Iterator<VariableEntity> i = clientVariableEntities.iterator(); i.hasNext();) {
+                final VariableEntity clientVariableEntity = i.next();
+                final VariableDTO clientVariable = clientVariableEntity.getVariable();
+
+                final Map<NodeIdentifier, Set<AffectedComponentEntity>> nodeAffectedComponentEntities = new HashMap<>();
+
+                boolean retainClientVariable = true;
+                for (final Map.Entry<NodeIdentifier, VariableRegistryEntity> nodeEntry : entityMap.entrySet()) {
+                    final VariableRegistryEntity nodeVariableRegistry = nodeEntry.getValue();
+                    final Set<VariableEntity> nodeVariableEntities = nodeVariableRegistry.getVariableRegistry().getVariables();
+
+                    // if this node has no variables, then the current client variable should be removed
+                    if (nodeVariableEntities == null) {
+                        retainClientVariable = false;
+                        break;
+                    }
+
+                    boolean variableFound = false;
+                    for (final VariableEntity nodeVariableEntity : nodeVariableEntities) {
+                        final VariableDTO nodeVariable = nodeVariableEntity.getVariable();
+
+                        // identify the current clientVariable for each node
+                        if (clientVariable.getProcessGroupId().equals(nodeVariable.getProcessGroupId()) && clientVariable.getName().equals(nodeVariable.getName())) {
+                            variableFound = true;
+
+                            if (Boolean.FALSE.equals(nodeVariableEntity.getCanWrite())) {
+                                clientVariableEntity.setCanWrite(false);
+                            }
+
+                            nodeAffectedComponentEntities.put(nodeEntry.getKey(), nodeVariableEntity.getVariable().getAffectedComponents());
+                            break;
+                        }
+                    }
+
+                    if (!variableFound) {
+                        retainClientVariable = false;
+                        break;
+                    }
+                }
+
+                if (!retainClientVariable) {
+                    i.remove();
+                } else {
+                    final Set<AffectedComponentEntity> clientAffectedComponentEntities = clientVariableEntity.getVariable().getAffectedComponents();
+                    affectedComponentEntityMerger.mergeAffectedComponents(clientAffectedComponentEntities, nodeAffectedComponentEntities);
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/AffectedComponentEntityMerger.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/AffectedComponentEntityMerger.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/AffectedComponentEntityMerger.java
new file mode 100644
index 0000000..60a605d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/manager/AffectedComponentEntityMerger.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+package org.apache.nifi.cluster.manager;
+
+import org.apache.nifi.cluster.protocol.NodeIdentifier;
+import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.web.api.dto.AffectedComponentDTO;
+import org.apache.nifi.web.api.dto.PermissionsDTO;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class AffectedComponentEntityMerger {
+
+    public void mergeAffectedComponents(final Set<AffectedComponentEntity> affectedComponents, final Map<NodeIdentifier, Set<AffectedComponentEntity>> affectedComponentMap) {
+
+        final Map<String, Integer> activeThreadCounts = new HashMap<>();
+        final Map<String, String> states = new HashMap<>();
+        final Map<String, PermissionsDTO> canReads = new HashMap<>();
+
+        for (final Map.Entry<NodeIdentifier, Set<AffectedComponentEntity>> nodeEntry : affectedComponentMap.entrySet()) {
+            final Set<AffectedComponentEntity> nodeAffectedComponents = nodeEntry.getValue();
+
+            // go through all the nodes referencing components
+            if (nodeAffectedComponents != null) {
+                for (final AffectedComponentEntity nodeAffectedComponentEntity : nodeAffectedComponents) {
+                    final AffectedComponentDTO nodeAffectedComponent = nodeAffectedComponentEntity.getComponent();
+
+                    if (nodeAffectedComponentEntity.getPermissions().getCanRead()) {
+                        // handle active thread counts
+                        if (nodeAffectedComponent.getActiveThreadCount() != null && nodeAffectedComponent.getActiveThreadCount() > 0) {
+                            final Integer current = activeThreadCounts.get(nodeAffectedComponent.getId());
+                            if (current == null) {
+                                activeThreadCounts.put(nodeAffectedComponent.getId(), nodeAffectedComponent.getActiveThreadCount());
+                            } else {
+                                activeThreadCounts.put(nodeAffectedComponent.getId(), nodeAffectedComponent.getActiveThreadCount() + current);
+                            }
+                        }
+
+                        // handle controller service state
+                        final String state = states.get(nodeAffectedComponent.getId());
+                        if (state == null) {
+                            if (ControllerServiceState.DISABLING.name().equals(nodeAffectedComponent.getState())) {
+                                states.put(nodeAffectedComponent.getId(), ControllerServiceState.DISABLING.name());
+                            } else if (ControllerServiceState.ENABLING.name().equals(nodeAffectedComponent.getState())) {
+                                states.put(nodeAffectedComponent.getId(), ControllerServiceState.ENABLING.name());
+                            }
+                        }
+                    }
+
+                    // handle read permissions
+                    final PermissionsDTO mergedPermissions = canReads.get(nodeAffectedComponentEntity.getId());
+                    final PermissionsDTO permissions = nodeAffectedComponentEntity.getPermissions();
+                    if (permissions != null) {
+                        if (mergedPermissions == null) {
+                            canReads.put(nodeAffectedComponentEntity.getId(), permissions);
+                        } else {
+                            PermissionsDtoMerger.mergePermissions(mergedPermissions, permissions);
+                        }
+                    }
+                }
+            }
+        }
+
+        // go through each affected components
+        if (affectedComponents != null) {
+            for (final AffectedComponentEntity affectedComponent : affectedComponents) {
+                final PermissionsDTO permissions = canReads.get(affectedComponent.getId());
+                if (permissions != null && permissions.getCanRead() != null && permissions.getCanRead()) {
+                    final Integer activeThreadCount = activeThreadCounts.get(affectedComponent.getId());
+                    if (activeThreadCount != null) {
+                        affectedComponent.getComponent().setActiveThreadCount(activeThreadCount);
+                    }
+
+                    final String state = states.get(affectedComponent.getId());
+                    if (state != null) {
+                        affectedComponent.getComponent().setState(state);
+                    }
+                } else {
+                    affectedComponent.setPermissions(permissions);
+                    affectedComponent.setComponent(null);
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 2b7b51d..1754cf7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -16,23 +16,7 @@
  */
 package org.apache.nifi.groups;
 
-import static java.util.Objects.requireNonNull;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.stream.Collectors;
-
+import com.google.common.collect.Sets;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.commons.lang3.builder.ToStringBuilder;
@@ -76,6 +60,7 @@ import org.apache.nifi.logging.LogRepositoryFactory;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarCloseable;
 import org.apache.nifi.processor.StandardProcessContext;
+import org.apache.nifi.registry.ComponentVariableRegistry;
 import org.apache.nifi.registry.VariableDescriptor;
 import org.apache.nifi.registry.variable.MutableVariableRegistry;
 import org.apache.nifi.remote.RemoteGroupPort;
@@ -87,7 +72,22 @@ import org.apache.nifi.web.api.dto.TemplateDTO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+import static java.util.Objects.requireNonNull;
 
 public final class StandardProcessGroup implements ProcessGroup {
 
@@ -2651,7 +2651,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         final Set<ConfiguredComponent> affected = new HashSet<>();
 
         // Determine any Processors that references the variable
-        for (final ProcessorNode processor : findAllProcessors()) {
+        for (final ProcessorNode processor : getProcessors()) {
             for (final VariableImpact impact : getVariableImpact(processor)) {
                 if (impact.isImpacted(variableName)) {
                     affected.add(processor);
@@ -2662,7 +2662,7 @@ public final class StandardProcessGroup implements ProcessGroup {
         // Determine any Controller Service that references the variable. If Service A references a variable,
         // then that means that any other component that references that service is also affected, so recursively
         // find any references to that service and add it.
-        for (final ControllerServiceNode service : findAllControllerServices()) {
+        for (final ControllerServiceNode service : getControllerServices(false)) {
             for (final VariableImpact impact : getVariableImpact(service)) {
                 if (impact.isImpacted(variableName)) {
                     affected.add(service);
@@ -2673,6 +2673,18 @@ public final class StandardProcessGroup implements ProcessGroup {
             }
         }
 
+        // For any child Process Group that does not override the variable, also include its references.
+        // If a child group has a value for the same variable, though, then that means that the child group
+        // is overriding the variable and its components are actually referencing a different variable.
+        for (final ProcessGroup childGroup : getProcessGroups()) {
+            final ComponentVariableRegistry childRegistry = childGroup.getVariableRegistry();
+            final VariableDescriptor descriptor = childRegistry.getVariableKey(variableName);
+            final boolean overridden = childRegistry.getVariableMap().containsKey(descriptor);
+            if (!overridden) {
+                affected.addAll(childGroup.getComponentsAffectedByVariable(variableName));
+            }
+        }
+
         return affected;
     }
 
@@ -2695,7 +2707,11 @@ public final class StandardProcessGroup implements ProcessGroup {
     }
 
     private List<VariableImpact> getVariableImpact(final ConfiguredComponent component) {
-        return component.getProperties().values().stream()
+        return component.getProperties().keySet().stream()
+            .map(descriptor -> {
+                final String configuredVal = component.getProperty(descriptor);
+                return configuredVal == null ? descriptor.getDefaultValue() : configuredVal;
+            })
             .map(propVal -> Query.prepare(propVal).getVariableImpact())
             .collect(Collectors.toList());
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/registry/variable/VariableRegistryUpdateRequest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/registry/variable/VariableRegistryUpdateRequest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/registry/variable/VariableRegistryUpdateRequest.java
index 82d4683..72f9a7a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/registry/variable/VariableRegistryUpdateRequest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/registry/variable/VariableRegistryUpdateRequest.java
@@ -17,17 +17,28 @@
 
 package org.apache.nifi.registry.variable;
 
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+
 import java.util.Date;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 public class VariableRegistryUpdateRequest {
     private final String requestId;
     private final String processGroupId;
+    private final NiFiUser user;
     private volatile Date submissionTime = new Date();
     private volatile Date lastUpdated = new Date();
     private volatile boolean complete = false;
 
     private final AtomicReference<String> failureReason = new AtomicReference<>();
+    private RevisionDTO processGroupRevision;
+    private Map<String, AffectedComponentEntity> affectedComponents;
 
     private final VariableRegistryUpdateStep identifyComponentsStep = new VariableRegistryUpdateStep("Identifying components affected");
     private final VariableRegistryUpdateStep stopProcessors = new VariableRegistryUpdateStep("Stopping affected Processors");
@@ -36,16 +47,17 @@ public class VariableRegistryUpdateRequest {
     private final VariableRegistryUpdateStep enableServices = new VariableRegistryUpdateStep("Re-Enabling affected Controller Services");
     private final VariableRegistryUpdateStep startProcessors = new VariableRegistryUpdateStep("Restarting affected Processors");
 
-    public VariableRegistryUpdateRequest(final String requestId, final String processGroupId) {
+    public VariableRegistryUpdateRequest(final String requestId, final String processGroupId, final Set<AffectedComponentEntity> affectedComponents, final NiFiUser user) {
         this.requestId = requestId;
         this.processGroupId = processGroupId;
+        this.affectedComponents = affectedComponents.stream().collect(Collectors.toMap(AffectedComponentEntity::getId, Function.identity()));
+        this.user = user;
     }
 
     public String getProcessGroupId() {
         return processGroupId;
     }
 
-
     public String getRequestId() {
         return requestId;
     }
@@ -54,6 +66,10 @@ public class VariableRegistryUpdateRequest {
         return submissionTime;
     }
 
+    public NiFiUser getUser() {
+        return user;
+    }
+
     public Date getLastUpdated() {
         return lastUpdated;
     }
@@ -102,6 +118,18 @@ public class VariableRegistryUpdateRequest {
         this.failureReason.set(reason);
     }
 
+    public RevisionDTO getProcessGroupRevision() {
+        return processGroupRevision;
+    }
+
+    public void setProcessGroupRevision(RevisionDTO processGroupRevision) {
+        this.processGroupRevision = processGroupRevision;
+    }
+
+    public Map<String, AffectedComponentEntity> getAffectedComponents() {
+        return affectedComponents;
+    }
+
     public void cancel() {
         this.failureReason.compareAndSet(null, "Update was cancelled");
         this.complete = true;

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 6fed58e..faa8c0e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -16,13 +16,6 @@
  */
 package org.apache.nifi.web;
 
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-
 import org.apache.nifi.authorization.AuthorizeAccess;
 import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.user.NiFiUser;
@@ -77,6 +70,7 @@ import org.apache.nifi.web.api.dto.status.ControllerStatusDTO;
 import org.apache.nifi.web.api.entity.AccessPolicyEntity;
 import org.apache.nifi.web.api.entity.ActionEntity;
 import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.BulletinEntity;
 import org.apache.nifi.web.api.entity.ConnectionEntity;
 import org.apache.nifi.web.api.entity.ConnectionStatusEntity;
@@ -108,6 +102,13 @@ import org.apache.nifi.web.api.entity.UserEntity;
 import org.apache.nifi.web.api.entity.UserGroupEntity;
 import org.apache.nifi.web.api.entity.VariableRegistryEntity;
 
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
 /**
  * Defines the NiFiServiceFacade interface.
  */
@@ -518,9 +519,10 @@ public interface NiFiServiceFacade {
      * Gets all the Processor transfer objects for this controller.
      *
      * @param groupId group
+     * @param includeDescendants if processors from descendent groups should be included
      * @return List of all the Processor transfer object
      */
-    Set<ProcessorEntity> getProcessors(String groupId);
+    Set<ProcessorEntity> getProcessors(String groupId, boolean includeDescendants);
 
     /**
      * Verifies the specified processor can be updated.
@@ -925,12 +927,21 @@ public interface NiFiServiceFacade {
     VariableRegistryEntity updateVariableRegistry(NiFiUser user, Revision revision, VariableRegistryDTO variableRegistryDto);
 
     /**
-     * Determines which components will be affected by updating the given Variable Registry
+     * Determines which components will be affected by updating the given Variable Registry.
+     *
+     * @param variableRegistryDto the variable registry
+     * @return the components that will be affected
+     */
+    Set<AffectedComponentEntity> getComponentsAffectedByVariableRegistryUpdate(VariableRegistryDTO variableRegistryDto);
+
+    /**
+     * Determines which components are active and will be affected by updating the given Variable Registry. These active components
+     * are needed to authorize the request and deactivate prior to changing the variables.
      *
      * @param variableRegistryDto the variable registry
      * @return the components that will be affected
      */
-    Set<AffectedComponentDTO> getComponentsAffectedByVariableRegistryUpdate(VariableRegistryDTO variableRegistryDto);
+    Set<AffectedComponentDTO> getActiveComponentsAffectedByVariableRegistryUpdate(VariableRegistryDTO variableRegistryDto);
 
     /**
      * Gets all process groups in the specified parent group.

http://git-wip-us.apache.org/repos/asf/nifi/blob/eac47e90/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 7cd5732..9caabd6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -161,6 +161,7 @@ import org.apache.nifi.web.api.entity.AccessPolicyEntity;
 import org.apache.nifi.web.api.entity.AccessPolicySummaryEntity;
 import org.apache.nifi.web.api.entity.ActionEntity;
 import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
 import org.apache.nifi.web.api.entity.BulletinEntity;
 import org.apache.nifi.web.api.entity.ComponentReferenceEntity;
 import org.apache.nifi.web.api.entity.ConnectionEntity;
@@ -790,7 +791,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public Set<AffectedComponentDTO> getComponentsAffectedByVariableRegistryUpdate(final VariableRegistryDTO variableRegistryDto) {
+    public Set<AffectedComponentDTO> getActiveComponentsAffectedByVariableRegistryUpdate(final VariableRegistryDTO variableRegistryDto) {
         final ProcessGroup group = processGroupDAO.getProcessGroup(variableRegistryDto.getProcessGroupId());
         if (group == null) {
             throw new ResourceNotFoundException("Could not find Process Group with ID " + variableRegistryDto.getProcessGroupId());
@@ -827,6 +828,29 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         return affectedComponentDtos;
     }
 
+    @Override
+    public Set<AffectedComponentEntity> getComponentsAffectedByVariableRegistryUpdate(final VariableRegistryDTO variableRegistryDto) {
+        final ProcessGroup group = processGroupDAO.getProcessGroup(variableRegistryDto.getProcessGroupId());
+        if (group == null) {
+            throw new ResourceNotFoundException("Could not find Process Group with ID " + variableRegistryDto.getProcessGroupId());
+        }
+
+        final Map<String, String> variableMap = new HashMap<>();
+        variableRegistryDto.getVariables().stream() // have to use forEach here instead of using Collectors.toMap because value may be null
+                .map(VariableEntity::getVariable)
+                .forEach(var -> variableMap.put(var.getName(), var.getValue()));
+
+        final Set<AffectedComponentEntity> affectedComponentEntities = new HashSet<>();
+
+        final Set<String> updatedVariableNames = getUpdatedVariables(group, variableMap);
+        for (final String variableName : updatedVariableNames) {
+            final Set<ConfiguredComponent> affectedComponents = group.getComponentsAffectedByVariable(variableName);
+            affectedComponentEntities.addAll(dtoFactory.createAffectedComponentEntities(affectedComponents, revisionManager));
+        }
+
+        return affectedComponentEntities;
+    }
+
     private Set<String> getUpdatedVariables(final ProcessGroup group, final Map<String, String> newVariableValues) {
         final Set<String> updatedVariableNames = new HashSet<>();
 
@@ -856,7 +880,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
         final RevisionUpdate<VariableRegistryDTO> snapshot = updateComponent(user, revision,
             processGroupNode,
             () -> processGroupDAO.updateVariableRegistry(variableRegistryDto),
-            processGroup -> dtoFactory.createVariableRegistryDto(processGroup));
+            processGroup -> dtoFactory.createVariableRegistryDto(processGroup, revisionManager));
 
         final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroupNode);
         final RevisionDTO updatedRevision = dtoFactory.createRevisionDTO(snapshot.getLastModification());
@@ -2498,8 +2522,8 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     @Override
-    public Set<ProcessorEntity> getProcessors(final String groupId) {
-        final Set<ProcessorNode> processors = processorDAO.getProcessors(groupId);
+    public Set<ProcessorEntity> getProcessors(final String groupId, final boolean includeDescendants) {
+        final Set<ProcessorNode> processors = processorDAO.getProcessors(groupId, includeDescendants);
         return processors.stream()
             .map(processor -> createProcessorEntity(processor))
             .collect(Collectors.toSet());
@@ -3231,7 +3255,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     }
 
     private VariableRegistryEntity createVariableRegistryEntity(final ProcessGroup processGroup, final boolean includeAncestorGroups) {
-        final VariableRegistryDTO registryDto = dtoFactory.createVariableRegistryDto(processGroup);
+        final VariableRegistryDTO registryDto = dtoFactory.createVariableRegistryDto(processGroup, revisionManager);
         final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(processGroup.getIdentifier()));
         final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup);
 
@@ -3240,7 +3264,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             while (parent != null) {
                 final PermissionsDTO parentPerms = dtoFactory.createPermissionsDto(parent);
                 if (Boolean.TRUE.equals(parentPerms.getCanRead())) {
-                    final VariableRegistryDTO parentRegistryDto = dtoFactory.createVariableRegistryDto(parent);
+                    final VariableRegistryDTO parentRegistryDto = dtoFactory.createVariableRegistryDto(parent, revisionManager);
                     final Set<VariableEntity> parentVariables = parentRegistryDto.getVariables();
                     registryDto.getVariables().addAll(parentVariables);
                 }
@@ -3260,7 +3284,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
             throw new ResourceNotFoundException("Could not find group with ID " + groupId);
         }
 
-        final VariableRegistryDTO registryDto = dtoFactory.populateAffectedComponents(variableRegistryDto, processGroup);
+        final VariableRegistryDTO registryDto = dtoFactory.populateAffectedComponents(variableRegistryDto, processGroup, revisionManager);
         final RevisionDTO revision = dtoFactory.createRevisionDTO(revisionManager.getRevision(processGroup.getIdentifier()));
         final PermissionsDTO permissions = dtoFactory.createPermissionsDto(processGroup);
         return entityFactory.createVariableRegistryEntity(registryDto, revision, permissions);