You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by bb...@apache.org on 2018/01/08 18:13:54 UTC

[06/50] nifi git commit: NIFI-4436: - Adding support to save a version of a flow based on a selected Process Group. - Adding support for revert changes back to the most recent version. - Adding support to disconnect from version control. - Moving the ver

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-state.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-state.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-state.js
index 8fb73a8..8f8bb6d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-state.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-state.js
@@ -230,7 +230,7 @@
                 applyFilter();
             });
 
-            // initialize the processor configuration dialog
+            // initialize the component state dialog
             $('#component-state-dialog').modal({
                 scrollableContentStyle: 'scrollable',
                 headerText: 'Component State',

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-version.js
index 3110e64..02abac1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-version.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-component-version.js
@@ -18,7 +18,7 @@
 /* global nf */
 
 /**
- * Views state for a given component.
+ * Handles changing the version of a component bundle.
  */
 (function (root, factory) {
     if (typeof define === 'function' && define.amd) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/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 a32a47f..f128086 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
@@ -97,7 +97,7 @@
      * @param {selection} selection         The selection of currently selected components
      */
     var canCreateTemplate = function (selection) {
-        return nfCanvasUtils.canWrite() && (selection.empty() || nfCanvasUtils.canRead(selection));
+        return nfCanvasUtils.canWriteCurrentGroup() && (selection.empty() || nfCanvasUtils.canRead(selection));
     };
 
     /**
@@ -106,7 +106,7 @@
      * @param {selection} selection         The selection of currently selected components
      */
     var canUploadTemplate = function (selection) {
-        return nfCanvasUtils.canWrite() && selection.empty();
+        return nfCanvasUtils.canWriteCurrentGroup() && selection.empty();
     };
 
     /**
@@ -370,6 +370,89 @@
     };
 
     /**
+     * Determines whether the current selection supports flow versioning.
+     *
+     * @param selection
+     */
+    var supportsFlowVersioning = function (selection) {
+        if (nfCanvasUtils.supportsFlowVersioning() === false) {
+            return false;
+        }
+
+        if (selection.empty()) {
+            return nfCanvasUtils.canReadCurrentGroup() && nfCanvasUtils.canWriteCurrentGroup();
+        }
+
+        if (isProcessGroup(selection) === true) {
+            return nfCanvasUtils.canRead(selection) && nfCanvasUtils.canModify(selection);
+        }
+
+        return false;
+    };
+
+    /**
+     * Determines whether the current selection supports starting flow versioning.
+     *
+     * @param selection
+     */
+    var supportsStartFlowVersioning = function (selection) {
+        // ensure this selection supports flow versioning above
+        if (supportsFlowVersioning(selection) === false) {
+            return false;
+        }
+
+        if (selection.empty()) {
+            // check bread crumbs for version control information in the current group
+            var breadcrumbEntities = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+            if (breadcrumbEntities.length > 0) {
+                var breadcrumbEntity = breadcrumbEntities[breadcrumbEntities.length - 1];
+                if (breadcrumbEntity.permissions.canRead) {
+                    return nfCommon.isUndefinedOrNull(breadcrumbEntity.breadcrumb.versionControlInformation);
+                } else {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        }
+
+        // check the selection for version control information
+        var processGroupData = selection.datum();
+        return nfCommon.isUndefinedOrNull(processGroupData.component.versionControlInformation);
+    };
+
+    /**
+     * Determines whether the current selection supports stopping flow versioning.
+     *
+     * @param selection
+     */
+    var supportsStopFlowVersioning = function (selection) {
+        // ensure this selection supports flow versioning above
+        if (supportsFlowVersioning(selection) === false) {
+            return false;
+        }
+
+        if (selection.empty()) {
+            // check bread crumbs for version control information in the current group
+            var breadcrumbEntities = nfNgBridge.injector.get('breadcrumbsCtrl').getBreadcrumbs();
+            if (breadcrumbEntities.length > 0) {
+                var breadcrumbEntity = breadcrumbEntities[breadcrumbEntities.length - 1];
+                if (breadcrumbEntity.permissions.canRead) {
+                    return nfCommon.isDefinedAndNotNull(breadcrumbEntity.breadcrumb.versionControlInformation);
+                } else {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        }
+
+        // check the selection for version control information
+        var processGroupData = selection.datum();
+        return nfCommon.isDefinedAndNotNull(processGroupData.component.versionControlInformation);
+    };
+
+    /**
      * Determines whether the current selection could have provenance.
      *
      * @param {selection} selection
@@ -548,6 +631,16 @@
         {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: 'version-menu-item', groupMenuItem: {clazz: 'fa', text: 'Version'}, menuItems: [
+            {id: 'start-version-control-menu-item', condition: supportsStartFlowVersioning, menuItem: {clazz: 'fa fa-floppy-o', text: 'Start version control', action: 'saveFlowVersion'}},
+            {separator: true},
+            {id: 'commit-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa fa-floppy-o', text: 'Commit local changes', action: 'saveFlowVersion'}},
+            {id: 'revert-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Revert local changes', action: 'revertFlowChanges'}},
+            {id: 'change-version-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Change version', action: 'changeFlowVersion'}},
+            {separator: true},
+            {id: 'stop-version-control-menu-item', condition: supportsStopFlowVersioning, menuItem: {clazz: 'fa', text: 'Stop version control', action: 'disconnectFlowVersioning'}}
+        ]},
+        {separator: true},
         {id: 'enter-group-menu-item', condition: isProcessGroup, menuItem: {clazz: 'fa fa-sign-in', text: 'Enter group', action: 'enterGroup'}},
         {separator: true},
         {id: 'start-menu-item', condition: isRunnable, menuItem: {clazz: 'fa fa-play', text: 'Start', action: 'start'}},

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
new file mode 100644
index 0000000..6315d30
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-flow-version.js
@@ -0,0 +1,476 @@
+/*
+ * 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 */
+
+/**
+ * Handles versioning.
+ */
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define(['jquery',
+                'nf.ng.Bridge',
+                'nf.ErrorHandler',
+                'nf.Dialog',
+                'nf.Common',
+                'nf.Client',
+                'nf.CanvasUtils',
+                'nf.ProcessGroup',
+                'nf.Graph'],
+            function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph) {
+                return (nf.FlowVersion = factory($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph));
+            });
+    } else if (typeof exports === 'object' && typeof module === 'object') {
+        module.exports = (nf.FlowVerison =
+            factory(require('jquery'),
+                require('nf.ng.Bridge'),
+                require('nf.ErrorHandler'),
+                require('nf.Dialog'),
+                require('nf.Common'),
+                require('nf.Client'),
+                require('nf.CanvasUtils'),
+                require('nf.ProcessGroup'),
+                require('nf.Graph')));
+    } else {
+        nf.FlowVersion = factory(root.$,
+            root.nf.ng.Bridge,
+            root.nf.ErrorHandler,
+            root.nf.Dialog,
+            root.nf.Common,
+            root.nf.Client,
+            root.nf.CanvasUtils,
+            root.nf.ProcessGroup,
+            root.nf.Graph);
+    }
+}(this, function ($, nfNgBridge, nfErrorHandler, nfDialog, nfCommon, nfClient, nfCanvasUtils, nfProcessGroup, nfGraph) {
+    'use strict';
+
+    /**
+     * Reset the dialog.
+     */
+    var resetDialog = function () {
+        $('#flow-version-registry-combo').combo('destroy').hide();
+        $('#flow-version-bucket-combo').combo('destroy').hide();
+
+        $('#flow-version-registry').text('').hide();
+        $('#flow-version-bucket').text('').hide();
+
+        $('#flow-version-name').val('');
+        $('#flow-version-description').val('');
+        $('#flow-version-change-comments').val('');
+
+        $('#flow-version-process-group-id').removeData('versionControlInformation').removeData('revision').text('');
+    };
+
+    /**
+     * Loads the buckets for the specified registryIdentifier for the current user.
+     *
+     * @param registryIdentifier
+     * @returns {*}
+     */
+    var loadBuckets = function (registryIdentifier) {
+        return $.ajax({
+            type: 'GET',
+            url: '../nifi-api/flow/registries/' + encodeURIComponent(registryIdentifier) + '/buckets',
+            dataType: 'json'
+        }).done(function (response) {
+            var buckets = [];
+
+            if (nfCommon.isDefinedAndNotNull(response.buckets) && response.buckets.length > 0) {
+                response.buckets.sort(function (a, b) {
+                    return a.bucket.name > b.bucket.name;
+                });
+
+                $.each(response.buckets, function (_, bucketEntity) {
+                    var bucket = bucketEntity.bucket;
+                    buckets.push({
+                        text: bucket.name,
+                        value: bucket.id,
+                        description: nfCommon.escapeHtml(bucket.description)
+                    });
+                });
+            } else {
+                buckets.push({
+                    text: 'No available buckets',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                });
+            }
+
+            // load the buckets
+            $('#flow-version-bucket-combo').combo('destroy').combo({
+                options: buckets,
+                select: selectBucket
+            });
+        }).fail(function () {
+            $('#save-flow-version-dialog').modal('refreshButtons');
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    /**
+     * Select handler for the registries combo.
+     *
+     * @param selectedOption
+     */
+    var selectRegistry = function (selectedOption) {
+        if (selectedOption.disabled === true) {
+            $('#flow-version-bucket-combo').combo('destroy').combo({
+                options: [{
+                    text: 'No available buckets',
+                    value: null,
+                    optionClass: 'unset',
+                    disabled: true
+                }]
+            });
+
+            $('#save-flow-version-dialog').modal('refreshButtons');
+        } else {
+            loadBuckets(selectedOption.value);
+        }
+    };
+
+    /**
+     * Select handler for the buckets combo.
+     *
+     * @param selectedOption
+     */
+    var selectBucket = function (selectedOption) {
+        $('#save-flow-version-dialog').modal('refreshButtons');
+    };
+
+    /**
+     * Saves a flow version.
+     *
+     * @returns {*}
+     */
+    var saveFlowVersion = function () {
+        var processGroupId = $('#flow-version-process-group-id').text();
+        var processGroupRevision = $('#flow-version-process-group-id').data('revision');
+
+        var saveFlowVersionRequest = {
+            processGroupRevision: nfClient.getRevision({
+                revision: {
+                    version: processGroupRevision.version
+                }
+            })
+        };
+
+        var versionControlInformation = $('#flow-version-process-group-id').data('versionControlInformation');
+        if (nfCommon.isDefinedAndNotNull(versionControlInformation)) {
+            saveFlowVersionRequest['versionedFlow'] = {
+                registryId: versionControlInformation.registryId,
+                bucketId: versionControlInformation.bucketId,
+                flowId: versionControlInformation.flowId,
+                flowName: $('#flow-version-name').val(),
+                description: $('#flow-version-description').val(),
+                comments: $('#flow-version-change-comments').val()
+            }
+        } else {
+            var selectedRegistry =  $('#flow-version-registry-combo').combo('getSelectedOption');
+            var selectedBucket =  $('#flow-version-bucket-combo').combo('getSelectedOption');
+
+            saveFlowVersionRequest['versionedFlow'] = {
+                registryId: selectedRegistry.value,
+                bucketId: selectedBucket.value,
+                flowName: $('#flow-version-name').val(),
+                description: $('#flow-version-description').val(),
+                comments: $('#flow-version-change-comments').val()
+            }
+        }
+
+        return $.ajax({
+            type: 'POST',
+            data: JSON.stringify(saveFlowVersionRequest),
+            url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
+    return {
+        init: function () {
+            // initialize the flow version dialog
+            $('#save-flow-version-dialog').modal({
+                scrollableContentStyle: 'scrollable',
+                headerText: 'Save Flow Version',
+                buttons: [{
+                    buttonText: 'Save',
+                    color: {
+                        base: '#728E9B',
+                        hover: '#004849',
+                        text: '#ffffff'
+                    },
+                    disabled: function () {
+                        if ($('#flow-version-registry-combo').is(':visible')) {
+                            var selectedRegistry =  $('#flow-version-registry-combo').combo('getSelectedOption');
+                            var selectedBucket =  $('#flow-version-bucket-combo').combo('getSelectedOption');
+
+                            if (nfCommon.isDefinedAndNotNull(selectedRegistry) && nfCommon.isDefinedAndNotNull(selectedBucket)) {
+                                return selectedRegistry.disabled === true || selectedBucket.disabled === true;
+                            } else {
+                                return true;
+                            }
+                        } else {
+                            return false;
+                        }
+                    },
+                    handler: {
+                        click: function () {
+                            var processGroupId = $('#flow-version-process-group-id').text();
+                            
+                            saveFlowVersion().done(function (response) {
+                                // refresh either selected PG or bread crumb to reflect connected/tracking status
+                                if (nfCanvasUtils.getGroupId() === processGroupId) {
+                                    nfNgBridge.injector.get('breadcrumbsCtrl').updateVersionControlInformation(processGroupId, response.versionControlInformation);
+                                    nfNgBridge.digest();
+                                } else {
+                                    nfProcessGroup.reload(processGroupId);
+                                }
+
+                                // close the dialog
+                                $('#save-flow-version-dialog').modal('hide');
+                            });
+                        }
+                    }
+                }, {
+                    buttonText: 'Cancel',
+                    color: {
+                        base: '#E3E8EB',
+                        hover: '#C7D2D7',
+                        text: '#004849'
+                    },
+                    handler: {
+                        click: function () {
+                            $(this).modal('hide');
+                        }
+                    }
+                }],
+                handler: {
+                    close: function () {
+                        resetDialog();
+                    }
+                }
+            });
+        },
+
+        /**
+         * Shows the flow version dialog.
+         *
+         * @param processGroupId
+         */
+        showFlowVersionDialog: function (processGroupId) {
+            return $.Deferred(function (deferred) {
+                $.ajax({
+                    type: 'GET',
+                    url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
+                    dataType: 'json'
+                }).done(function (response) {
+                    // record the revision
+                    $('#flow-version-process-group-id').data('revision', response.processGroupRevision).text(processGroupId);
+
+                    if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
+                        var versionControlInformation = response.versionControlInformation;
+
+                        // update the registry and bucket visibility
+                        $('#flow-version-registry').text(versionControlInformation.registryId).show();
+                        $('#flow-version-bucket').text(versionControlInformation.bucketId).show();
+
+                        $('#flow-version-name').val('');
+                        $('#flow-version-description').val('');
+
+                        // record the versionControlInformation
+                        $('#flow-version-process-group-id').data('versionControlInformation', versionControlInformation)
+
+                        deferred.resolve();
+                    } else {
+                        // update the registry and bucket visibility
+                        $('#flow-version-registry-combo').show();
+                        $('#flow-version-bucket-combo').show();
+
+                        $.ajax({
+                            type: 'GET',
+                            url: '../nifi-api/flow/registries',
+                            dataType: 'json'
+                        }).done(function (registriesResponse) {
+                            var registries = [];
+
+                            if (nfCommon.isDefinedAndNotNull(registriesResponse.registries) && registriesResponse.registries.length > 0) {
+                                registriesResponse.registries.sort(function (a, b) {
+                                    return a.component.name > b.component.name;
+                                });
+
+                                $.each(registriesResponse.registries, function (_, registryEntity) {
+                                    var registry = registryEntity.component;
+                                    registries.push({
+                                        text: registry.name,
+                                        value: registry.id,
+                                        description: nfCommon.escapeHtml(registry.description)
+                                    });
+                                });
+                            } else {
+                                registries.push({
+                                    text: 'No available registries',
+                                    value: null,
+                                    optionClass: 'unset',
+                                    disabled: true
+                                });
+                            }
+
+                            // load the registries
+                            $('#flow-version-registry-combo').combo({
+                                options: registries,
+                                select: selectRegistry
+                            });
+
+                            deferred.resolve();
+                        }).fail(function () {
+                            deferred.reject();
+                        }).fail(nfErrorHandler.handleAjaxError);
+                    }
+                }).fail(nfErrorHandler.handleAjaxError);
+            }).done(function () {
+                $('#save-flow-version-dialog').modal('show');
+            }).fail(function () {
+                $('#save-flow-version-dialog').modal('refreshButtons');
+            }).promise();
+        },
+
+        /**
+         * Reverts changes for the specified Process Group.
+         *
+         * @param processGroupId
+         */
+        revertFlowChanges: function (processGroupId) {
+            // prompt the user before reverting
+            nfDialog.showYesNoDialog({
+                headerText: 'Revert Changes',
+                dialogContent: 'Are you sure you want to revert changes? All flow configuration changes will be reverted to the last version.',
+                noText: 'Cancel',
+                yesText: 'Revert',
+                yesHandler: function () {
+                    $.ajax({
+                        type: 'GET',
+                        url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
+                        dataType: 'json'
+                    }).done(function (response) {
+                        if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
+                            var revertFlowVersionRequest = {
+                                processGroupRevision: nfClient.getRevision({
+                                    revision: {
+                                        version: response.processGroupRevision.version
+                                    }
+                                }),
+                                versionControlInformation: response.versionControlInformation
+                            };
+
+                            $.ajax({
+                                type: 'POST',
+                                data: JSON.stringify(revertFlowVersionRequest),
+                                url: '../nifi-api/versions/revert-requests/process-groups/' + encodeURIComponent(processGroupId),
+                                dataType: 'json',
+                                contentType: 'application/json'
+                            }).done(function (response) {
+                                // TODO update multi step to show user the ramifications of reverting for confirmation
+
+                                if (nfCanvasUtils.getGroupId() === processGroupId) {
+                                    // if reverting current PG... reload/refresh this group/canvas
+                                    // TODO consider implementing this differently
+                                    $.ajax({
+                                        type: 'GET',
+                                        url: '../nifi-api/flow/process-groups/' + encodeURIComponent(processGroupId),
+                                        dataType: 'json'
+                                    }).done(function (response) {
+                                        nfGraph.set(response.processGroupFlow.flow);
+                                    }).fail(nfErrorHandler.handleAjaxError);
+                                } else {
+                                    // if reverting selected PG... reload selected PG to update counts, etc
+                                    nfProcessGroup.reload(processGroupId);
+                                }
+
+                                nfDialog.showOkDialog({
+                                    headerText: 'Revert Changes',
+                                    dialogContent: 'This Process Group has been reverted.'
+                                });
+                            }).fail(nfErrorHandler.handleAjaxError);
+                        } else {
+                            nfDialog.showOkDialog({
+                                headerText: 'Revert Changes',
+                                dialogContent: 'This Process Group is not currently under version control.'
+                            });
+                        }
+                    }).fail(nfErrorHandler.handleAjaxError);
+                }
+            });
+        },
+
+        /**
+         * Disconnects the specified Process Group from flow versioning.
+         *
+         * @param processGroupId
+         */
+        disconnectFlowVersioning: function (processGroupId) {
+            // prompt the user before disconnecting
+            nfDialog.showYesNoDialog({
+                headerText: 'Disconnect',
+                dialogContent: 'Are you sure you want to disconnect?',
+                noText: 'Cancel',
+                yesText: 'Disconnect',
+                yesHandler: function () {
+                    $.ajax({
+                        type: 'GET',
+                        url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId),
+                        dataType: 'json'
+                    }).done(function (response) {
+                        if (nfCommon.isDefinedAndNotNull(response.versionControlInformation)) {
+                            var revision = nfClient.getRevision({
+                                revision: {
+                                    version: response.processGroupRevision.version
+                                }
+                            });
+
+                            $.ajax({
+                                type: 'DELETE',
+                                url: '../nifi-api/versions/process-groups/' + encodeURIComponent(processGroupId) + '?' + $.param(revision),
+                                dataType: 'json',
+                                contentType: 'application/json'
+                            }).done(function (response) {
+                                // refresh either selected PG or bread crumb to reflect disconnected status
+                                if (nfCanvasUtils.getGroupId() === processGroupId) {
+                                    nfNgBridge.injector.get('breadcrumbsCtrl').updateVersionControlInformation(processGroupId, undefined);
+                                    nfNgBridge.digest();
+                                } else {
+                                    nfProcessGroup.reload(processGroupId);
+                                }
+
+                                nfDialog.showOkDialog({
+                                    headerText: 'Disconnect',
+                                    dialogContent: 'This Process Group has been disconnected.'
+                                });
+                            }).fail(nfErrorHandler.handleAjaxError);
+                        } else {
+                            nfDialog.showOkDialog({
+                                headerText: 'Disconnect',
+                                dialogContent: 'This Process Group is not currently under version control.'
+                            })
+                        }
+                    }).fail(nfErrorHandler.handleAjaxError);
+                }
+            });
+        }
+    };
+}));
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
index 498fbd7..58402df 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group-configuration.js
@@ -215,7 +215,7 @@
                         $('#process-group-configuration').data('process-group', {
                             'permissions': {
                                 canRead: false,
-                                canWrite: nfCanvasUtils.canWrite()
+                                canWrite: nfCanvasUtils.canWriteCurrentGroup()
                             }
                         });
                     } else {

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
index 13bbc70..17bb069 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-process-group.js
@@ -180,6 +180,15 @@
                 'class': 'process-group-name'
             });
 
+        // process group name
+        processGroup.append('text')
+            .attr({
+                'x': 3,
+                'y': 17,
+                'class': 'version-control'
+            })
+            .text('\uf00c');
+
         // always support selecting and navigation
         processGroup.on('dblclick', function (d) {
             // enter this group on double click
@@ -868,12 +877,18 @@
                         }).append('title').text(function (d) {
                         return d.component.name;
                     });
+
+                    // update version control information
+                    processGroup.select('text.version-control').style('visibility', nfCommon.isDefinedAndNotNull(processGroupData.component.versionControlInformation) ? 'visible' : 'hidden');
                 } else {
                     // clear the process group comments
                     details.select('text.process-group-comments').text(null);
 
                     // clear the process group name
                     processGroup.select('text.process-group-name').text(null);
+
+                    // update version control information
+                    processGroup.select('text.version-control').style('visibility', false);
                 }
 
                 // populate the stats

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
index 4da6fc6..0e5a2d7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-settings.js
@@ -81,7 +81,9 @@
             controllerConfig: '../nifi-api/controller/config',
             reportingTaskTypes: '../nifi-api/flow/reporting-task-types',
             createReportingTask: '../nifi-api/controller/reporting-tasks',
-            reportingTasks: '../nifi-api/flow/reporting-tasks'
+            reportingTasks: '../nifi-api/flow/reporting-tasks',
+            createRegistry: '../nifi-api/controller/registries',
+            registries: '../nifi-api/flow/registries'
         }
     };
 
@@ -465,6 +467,94 @@
     };
 
     /**
+     * Adds the specified entity.
+     */
+    var addRegistry = function () {
+        var registryEntity = {
+            'revision': nfClient.getRevision({
+                'revision': {
+                    'version': 0
+                }
+            }),
+            'component': {
+                'name': $('#registry-name').val(),
+                'uri': $('#registry-location').val(),
+                'description': $('#registry-description').val()
+            }
+        };
+
+        // add the new registry
+        var addRegistry = $.ajax({
+            type: 'POST',
+            url: config.urls.createRegistry,
+            data: JSON.stringify(registryEntity),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (registryEntity) {
+            // add the item
+            var registriesGrid = $('#registries-table').data('gridInstance');
+            var registriesData = registriesGrid.getData();
+            registriesData.addItem($.extend({
+                type: 'Registry'
+            }, registryEntity));
+
+            // resort
+            registriesData.reSort();
+            registriesGrid.invalidate();
+
+            // select the new reporting task
+            var row = registriesData.getRowById(registryEntity.id);
+            nfFilteredDialogCommon.choseRow(registriesGrid, row);
+            registriesGrid.scrollRowIntoView(row);
+        }).fail(nfErrorHandler.handleAjaxError);
+
+        // hide the dialog
+        $('#registry-configuration-dialog').modal('hide');
+
+        return addRegistry;
+    };
+
+    /**
+     * Updates the registry with the specified id.
+     *
+     * @param registryId
+     */
+    var updateRegistry = function (registryId) {
+        var registriesGrid = $('#registries-table').data('gridInstance');
+        var registriesData = registriesGrid.getData();
+
+        var registryEntity = registriesData.getItemById(registryId);
+        var requestRegistryEntity = {
+            'revision': nfClient.getRevision(registryEntity),
+            'component': {
+                'id': registryId,
+                'name': $('#registry-name').val(),
+                'uri': $('#registry-location').val(),
+                'description': $('#registry-description').val()
+            }
+        };
+
+        // add the new reporting task
+        var updateRegistry = $.ajax({
+            type: 'PUT',
+            url: registryEntity.uri,
+            data: JSON.stringify(requestRegistryEntity),
+            dataType: 'json',
+            contentType: 'application/json'
+        }).done(function (registryEntity) {
+            // add the item
+            registriesData.updateItem(registryId, $.extend({
+                type: 'Registry'
+            }, registryEntity));
+        }).fail(nfErrorHandler.handleAjaxError);
+
+        // hide the dialog
+        $('#registry-configuration-dialog').modal('hide');
+
+        return updateRegistry;
+    };
+
+    /**
      * Initializes the new reporting task dialog.
      */
     var initNewReportingTaskDialog = function () {
@@ -783,6 +873,20 @@
                 }
             }
         });
+
+        // initialize the registry configuration dialog
+        $('#registry-configuration-dialog').modal({
+            scrollableContentStyle: 'scrollable',
+            headerText: 'Add Registry',
+            handler: {
+                close: function () {
+                    $('#registry-id').text('');
+                    $('#registry-name').val('');
+                    $('#registry-location').val('');
+                    $('#registry-description').val('');
+                }
+            }
+        });
     };
 
     /**
@@ -1090,6 +1194,228 @@
         });
     };
 
+    var initRegistriesTable = function () {
+
+        var locationFormatter = function (row, cell, value, columnDef, dataContext) {
+            if (!dataContext.permissions.canRead) {
+                return '<span class="blank">' + nfCommon.escapeHtml(dataContext.id) + '</span>';
+            }
+
+            return nfCommon.escapeHtml(dataContext.component.uri);
+        };
+
+        var descriptionFormatter = function (row, cell, value, columnDef, dataContext) {
+            if (!dataContext.permissions.canRead) {
+                return '<span class="blank">' + nfCommon.escapeHtml(dataContext.id) + '</span>';
+            }
+
+            return nfCommon.escapeHtml(dataContext.component.description);
+        };
+
+        var registriesActionFormatter = function (row, cell, value, columnDef, dataContext) {
+            var markup = '';
+
+            if (nfCommon.canModifyController()) {
+                // edit registry
+                markup += '<div title="Edit" class="pointer edit-registry fa fa-pencil" style="margin-top: 2px; margin-right: 3px;" ></div>';
+
+                // remove registry
+                markup += '<div title="Remove" class="pointer remove-registry fa fa-trash" style="margin-top: 2px; margin-right: 3px;" ></div>';
+            }
+
+            return markup;
+        };
+
+        // define the column model for the reporting tasks table
+        var registriesColumnModel = [
+            {
+                id: 'name',
+                name: 'Name',
+                field: 'name',
+                formatter: nameFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'uri',
+                name: 'Location',
+                field: 'uri',
+                formatter: locationFormatter,
+                sortable: true,
+                resizable: true
+            },
+            {
+                id: 'description',
+                name: 'Description',
+                field: 'description',
+                formatter: descriptionFormatter,
+                sortable: true,
+                resizable: true
+            }
+        ];
+
+        // action column should always be last
+        registriesColumnModel.push({
+            id: 'actions',
+            name: '&nbsp;',
+            resizable: false,
+            formatter: registriesActionFormatter,
+            sortable: false,
+            width: 90,
+            maxWidth: 90
+        });
+
+        // initialize the dataview
+        var registriesData = new Slick.Data.DataView({
+            inlineFilters: false
+        });
+        registriesData.setItems([]);
+
+        // initialize the sort
+        sort({
+            columnId: 'name',
+            sortAsc: true
+        }, registriesData);
+
+        // initialize the grid
+        var registriesGrid = new Slick.Grid('#registries-table', registriesData, registriesColumnModel, gridOptions);
+        registriesGrid.setSelectionModel(new Slick.RowSelectionModel());
+        registriesGrid.registerPlugin(new Slick.AutoTooltips());
+        registriesGrid.setSortColumn('name', true);
+        registriesGrid.onSort.subscribe(function (e, args) {
+            sort({
+                columnId: args.sortCol.id,
+                sortAsc: args.sortAsc
+            }, registriesData);
+        });
+
+        // configure a click listener
+        registriesGrid.onClick.subscribe(function (e, args) {
+            var target = $(e.target);
+
+            // get the service at this row
+            var registryEntity = registriesData.getItem(args.row);
+
+            // determine the desired action
+            if (registriesGrid.getColumns()[args.cell].id === 'actions') {
+                if (target.hasClass('edit-registry')) {
+                    editRegistry(registryEntity);
+                } else if (target.hasClass('remove-registry')) {
+                    promptToRemoveRegistry(registryEntity);
+                }
+            } else if (registriesGrid.getColumns()[args.cell].id === 'moreDetails') {
+                // if (target.hasClass('view-reporting-task')) {
+                //     nfReportingTask.showDetails(reportingTaskEntity);
+                // } else if (target.hasClass('reporting-task-usage')) {
+                //     // close the settings dialog
+                //     $('#shell-close-button').click();
+                //
+                //     // open the documentation for this reporting task
+                //     nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                //         select: reportingTaskEntity.component.type,
+                //         group: reportingTaskEntity.component.bundle.group,
+                //         artifact: reportingTaskEntity.component.bundle.artifact,
+                //         version: reportingTaskEntity.component.bundle.version
+                //     })).done(function () {
+                //         nfSettings.showSettings();
+                //     });
+                // }
+            }
+        });
+
+        // wire up the dataview to the grid
+        registriesData.onRowCountChanged.subscribe(function (e, args) {
+            registriesGrid.updateRowCount();
+            registriesGrid.render();
+        });
+        registriesData.onRowsChanged.subscribe(function (e, args) {
+            registriesGrid.invalidateRows(args.rows);
+            registriesGrid.render();
+        });
+        registriesData.syncGridSelection(registriesGrid, true);
+
+        // hold onto an instance of the grid
+        $('#registries-table').data('gridInstance', registriesGrid);
+    };
+
+    /**
+     * Edits the specified registry entity.
+     *
+     * @param registryEntity
+     */
+    var editRegistry = function (registryEntity) {
+        // populate the dialog
+        $('#registry-id').text(registryEntity.id);
+        $('#registry-name').val(registryEntity.component.name);
+        $('#registry-location').val(registryEntity.component.uri);
+        $('#registry-description').val(registryEntity.component.description);
+
+        // show the dialog
+        $('#registry-configuration-dialog').modal('setButtonModel', [{
+            buttonText: 'Update',
+            color: {
+                base: '#728E9B',
+                hover: '#004849',
+                text: '#ffffff'
+            },
+            handler: {
+                click: function () {
+                    updateRegistry(registryEntity.id);
+                }
+            }
+        }, {
+            buttonText: 'Cancel',
+            color: {
+                base: '#E3E8EB',
+                hover: '#C7D2D7',
+                text: '#004849'
+            },
+            handler: {
+                click: function () {
+                    $(this).modal('hide');
+                }
+            }
+        }]).modal('show');
+    };
+
+    /**
+     * Prompts the user before attempting to delete the specified registry.
+     *
+     * @param {object} registryEntity
+     */
+    var promptToRemoveRegistry = function (registryEntity) {
+        // prompt for deletion
+        nfDialog.showYesNoDialog({
+            headerText: 'Delete Registry',
+            dialogContent: 'Delete registry \'' + nfCommon.escapeHtml(registryEntity.component.name) + '\'?',
+            yesHandler: function () {
+                removeRegistry(registryEntity);
+            }
+        });
+    };
+
+    /**
+     * Deletes the specified registry.
+     *
+     * @param {object} registryEntity
+     */
+    var removeRegistry = function (registryEntity) {
+        var revision = nfClient.getRevision(registryEntity);
+        $.ajax({
+            type: 'DELETE',
+            url: registryEntity.uri + '?' + $.param({
+                version: revision.version,
+                clientId: revision.clientId
+            }),
+            dataType: 'json'
+        }).done(function (response) {
+            // remove the task
+            var registryGrid = $('#registries-table').data('gridInstance');
+            var registryData = registryGrid.getData();
+            registryData.deleteItem(registryEntity.id);
+        }).fail(nfErrorHandler.handleAjaxError);
+    };
+
     /**
      * Loads the settings.
      */
@@ -1158,6 +1484,9 @@
         // load the reporting tasks
         var reportingTasks = loadReportingTasks();
 
+        // load the registries
+        var registries = loadRegistries();
+
         // return a deferred for all parts of the settings
         return $.when(settings, controllerServicesXhr, reportingTasks).done(function (settingsResult, controllerServicesResult) {
             var controllerServicesResponse = controllerServicesResult[0];
@@ -1199,6 +1528,32 @@
     };
 
     /**
+     * Loads the registries.
+     */
+    var loadRegistries = function () {
+        return $.ajax({
+            type: 'GET',
+            url: config.urls.registries,
+            dataType: 'json'
+        }).done(function (response) {
+            var registries = [];
+            $.each(response.registries, function (_, registryEntity) {
+                registries.push($.extend({
+                    type: 'Registry'
+                }, registryEntity));
+            });
+
+            var registriesGrid = $('#registries-table').data('gridInstance');
+            var registriesData = registriesGrid.getData();
+
+            // update the registries
+            registriesData.setItems(registries);
+            registriesData.reSort();
+            registriesGrid.invalidate();
+        });
+    };
+
+    /**
      * Shows the process group configuration.
      */
     var showSettings = function () {
@@ -1241,6 +1596,9 @@
                 }, {
                     name: 'Reporting Tasks',
                     tabContentId: 'reporting-tasks-tab-content'
+                }, {
+                    name: 'Registries',
+                    tabContentId: 'registries-tab-content'
                 }],
                 select: function () {
                     var tab = $(this).text();
@@ -1267,6 +1625,9 @@
                                 } else if (tab === 'Reporting Tasks') {
                                     $('#settings-save').hide();
                                     return 'Create a new reporting task';
+                                } else if (tab === 'Registries') {
+                                    $('#settings-save').hide();
+                                    return 'Register a new registry';
                                 }
                             });
                         } else {
@@ -1276,7 +1637,7 @@
 
                         if (tab === 'Reporting Task Controller Services') {
                             $('#controller-cs-availability').show();
-                        } else if (tab === 'Reporting Tasks') {
+                        } else if (tab === 'Reporting Tasks' || tab === 'Registries') {
                             $('#controller-cs-availability').hide();
                         }
 
@@ -1315,6 +1676,32 @@
 
                     // set the initial focus
                     $('#reporting-task-type-filter').focus();
+                } else if (selectedTab === 'Registries') {
+                    $('#registry-configuration-dialog').modal('setButtonModel', [{
+                        buttonText: 'Add',
+                        color: {
+                            base: '#728E9B',
+                            hover: '#004849',
+                            text: '#ffffff'
+                        },
+                        handler: {
+                            click: function () {
+                                addRegistry();
+                            }
+                        }
+                    }, {
+                        buttonText: 'Cancel',
+                        color: {
+                            base: '#E3E8EB',
+                            hover: '#C7D2D7',
+                            text: '#004849'
+                        },
+                        handler: {
+                            click: function () {
+                                $(this).modal('hide');
+                            }
+                        }
+                    }]).modal('show');
                 }
             });
 
@@ -1322,6 +1709,7 @@
             initGeneral();
             nfControllerServices.init(getControllerServicesTable(), nfSettings.showSettings);
             initReportingTasks();
+            initRegistriesTable();
         },
 
         /**

http://git-wip-us.apache.org/repos/asf/nifi/blob/7a0a900a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
index 10d36f1..a8b8579 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/views/nf-ng-breadcrumbs-directive-view.html
@@ -22,6 +22,7 @@ limitations under the License.
                 <span ng-if="separatorFunc(crumb.parentBreadcrumb)" style="margin: 0 12px;">
                     &raquo;
                 </span>
+                <span ng-if="separatorFunc(crumb.breadcrumb.versionControlInformation)" class="breadcrumb-version-control fa fa-check" style="margin: 0 6px;"></span>
                 <span class="link"
                       ng-class="(highlightCrumbId === crumb.id) ? 'link-bold' : ''"
                       ng-click="clickFunc(crumb.id)">