Repository: nifi-registry
Updated Branches:
  refs/heads/master a2f639f37 -> 247985ada


http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/247985ad/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js 
b/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
index e9fbb5f..f796f7a 100644
--- a/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
+++ b/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
@@ -20,105 +20,108 @@ var ngRouter = require('@angular/router');
 var fdsDialogsModule = require('@fluid-design-system/dialogs');
 var fdsSnackBarsModule = require('@fluid-design-system/snackbars');
 var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var NfStorage = require('nifi-registry/services/nf-storage.service.js');
+var rxjs = require('rxjs/Rx');
 require('rxjs/add/operator/catch');
 require('rxjs/add/operator/map');
 
-function filterData(data, searchTerm, ignoreCase) {
-    var field = '';
-    if (searchTerm.indexOf(":") > -1) {
-        field = searchTerm.split(':')[0].trim();
-        searchTerm = searchTerm.split(':')[1].trim();
-    }
-    var filter = searchTerm ? (ignoreCase ? searchTerm.toLowerCase() : 
searchTerm) : '';
-
-    if (filter) {
-        data = data.filter(function (item) {
-            var res = Object.keys(item).find(function (key) {
-                if (key !== field && field !== '') {
-                    return false;
-                }
-                var preItemValue = ('' + item[key]);
-                var itemValue = ignoreCase ? preItemValue.toLowerCase() : 
preItemValue;
-                return itemValue.indexOf(filter) > -1;
-            });
-            return !(typeof res === 'undefined');
-        });
-    }
-    return data;
-};
-
 /**
  * NfRegistryService constructor.
  *
- * @param TdDataTableService    The covalent data table service module.
- * @param NfRegistryApi         The registry API module.
- * @param Router                The angular router module.
- * @param FdsDialogService      The FDS dialog service.
- * @param FdsSnackBarService    The FDS snack bar service module.
+ * @param nfRegistryApi         The api service.
+ * @param nfStorage             A wrapper for the browser's local storage.
+ * @param tdDataTableService    The covalent data table service module.
+ * @param router                The angular router module.
+ * @param fdsDialogService      The FDS dialog service.
+ * @param fdsSnackBarService    The FDS snack bar service module.
  * @constructor
  */
-function NfRegistryService(TdDataTableService, NfRegistryApi, Router, 
FdsDialogService, FdsSnackBarService) {
-    this.router = Router;
-    this.dialogService = FdsDialogService;
-    this.snackBarService = FdsSnackBarService;
+function NfRegistryService(nfRegistryApi, nfStorage, tdDataTableService, 
router, fdsDialogService, fdsSnackBarService) {
+    this.router = router;
+    this.api = nfRegistryApi;
+    this.nfStorage = nfStorage;
+    this.dialogService = fdsDialogService;
+    this.snackBarService = fdsSnackBarService;
     this.registry = {
         name: "Nifi Registry"
     };
-    this.api = NfRegistryApi;
     this.bucket = {};
     this.buckets = [];
     this.droplet = {};
     this.droplets = [];
+    this.currentUser = {};
     this.user = {};
+    this.group = {};
     this.users = [];
+    this.groups = [];
     this.alerts = [];
     this.explorerViewType = '';
     this.perspective = '';
     this.breadCrumbState = 'out';
-    this.dataTableService = TdDataTableService;
+    this.dataTableService = tdDataTableService;
 
     this.filteredDroplets = [];
     this.dropletActions = [{
-        'name': 'Delete',
-        'icon': 'fa fa-trash',
-        'tooltip': 'Delete'
+        name: 'Delete',
+        icon: 'fa fa-trash',
+        tooltip: 'Delete'
     }];
     this.dropletColumns = [
-        {name: 'name', label: 'Name', sortable: true},
-        {name: 'updated', label: 'Updated', sortable: true}
+        {
+            name: 'name',
+            label: 'Name',
+            sortable: true
+        },
+        {
+            name: 'updated',
+            label: 'Updated',
+            sortable: true
+        }
     ];
     this.autoCompleteDroplets = [];
     this.dropletsSearchTerms = [];
 
     this.filteredBuckets = [];
     this.bucketColumns = [
-        {name: 'name', label: 'Bucket Name', sortable: true, tooltip: 'Sort 
Buckets by name.'}
+        {
+            name: 'name',
+            label: 'Bucket Name',
+            sortable: true,
+            tooltip: 'Sort Buckets by name.'
+        }
     ];
     this.allBucketsSelected = false;
     this.autoCompleteBuckets = [];
     this.selectedBuckets = [];
     this.bucketsSearchTerms = [];
-    this.disableMultiBucketActions = true;
+    this.isMultiBucketActionsDisabled = true;
 
     this.filteredUsers = [];
+    this.filteredUserGroups = [];
     this.userColumns = [
-        {name: 'status', label: 'Status', sortable: true, tooltip: 'User 
Status.', width: 18},
-        {name: 'name', label: 'Name', sortable: true, tooltip: 'User name.', 
width: 30},
-        {name: 'provider', label: 'Provider', sortable: true, tooltip: 
'Authentication provider.', width: 30}
+        {
+            name: 'identity',
+            label: 'Display Name',
+            sortable: true,
+            tooltip: 'User name.',
+            width: 100
+        }
     ];
-    this.allUsersSelected = false;
-    this.autoCompleteUsers = [];
+    this.allUsersAndGroupsSelected = false;
+    this.autoCompleteUsersAndGroups = [];
     this.selectedUsers = [];
     this.usersSearchTerms = [];
+    this.isMultiUserActionsDisabled = true;
+    this.isMultiUserGroupActionsDisabled = false;
 };
 
 NfRegistryService.prototype = {
     constructor: NfRegistryService,
 
     /**
-     * Set the state for the breadcrumb animations.
+     * Set the `breadCrumbState` for the breadcrumb animations.
      *
-     * @param {string} state The state. Valid values are 'in' or 'out'.
+     * @param {string} state    The state. Valid values are 'in' or 'out'.
      */
     setBreadcrumbState: function (state) {
         this.breadCrumbState = state;
@@ -156,7 +159,7 @@ NfRegistryService.prototype = {
     /**
      * Generates the droplet grid-list explorer component's sorting menu 
options.
      *
-     * @param col   One of the available `dropletColumns`.
+     * @param col           One of the available `dropletColumns`.
      * @returns {string}
      */
     generateSortMenuLabels: function (col) {
@@ -191,31 +194,20 @@ NfRegistryService.prototype = {
                 function (accept) {
                     if (accept) {
                         
self.api.deleteDroplet(droplet.link.href).subscribe(function (response) {
-                            if (response.link === null) {
-                                self.droplets = self.droplets.filter(function 
(d) {
-                                    return (d.identifier !== 
droplet.identifier) ? true : false
-                                });
-                                var snackBarRef = 
self.snackBarService.openCoaster({
-                                    title: 'Success',
-                                    message: 'All versions of this ' + 
droplet.type.toLowerCase() + ' have been deleted.',
-                                    verticalPosition: 'bottom',
-                                    horizontalPosition: 'right',
-                                    icon: 'fa fa-check-circle-o',
-                                    color: '#1EB475',
-                                    duration: 3000
-                                });
-                                self.droplet = {};
-                                self.filterDroplets();
-                            } else {
-                                var snackBarRef = 
self.snackBarService.openCoaster({
-                                    title: 'Error',
-                                    message: 'Error deleting ' + droplet.name 
+ '.',
-                                    verticalPosition: 'bottom',
-                                    horizontalPosition: 'right',
-                                    icon: 'fa fa-times-circle-o',
-                                    color: '#EF6162'
-                                });
-                            }
+                            self.droplets = self.droplets.filter(function (d) {
+                                return (d.identifier !== droplet.identifier) ? 
true : false
+                            });
+                            var snackBarRef = 
self.snackBarService.openCoaster({
+                                title: 'Success',
+                                message: 'All versions of this ' + 
droplet.type.toLowerCase() + ' have been deleted.',
+                                verticalPosition: 'bottom',
+                                horizontalPosition: 'right',
+                                icon: 'fa fa-check-circle-o',
+                                color: '#1EB475',
+                                duration: 3000
+                            });
+                            self.droplet = {};
+                            self.filterDroplets();
                         });
                     }
                 });
@@ -234,7 +226,7 @@ NfRegistryService.prototype = {
     },
 
     /**
-     * Sort `droplets` by `column`.
+     * Sort `filteredDroplets` by `column`.
      *
      * @param column    The column to sort by.
      */
@@ -270,7 +262,6 @@ NfRegistryService.prototype = {
             for (var i = 0; i < arrayLength; i++) {
                 if (this.dropletColumns[i].sortable === true) {
                     sortBy = this.dropletColumns[i].name;
-                    this.activeDropletColumn = this.dropletColumns[i];
                     //only one column can be actively sorted so we reset all 
to inactive
                     this.dropletColumns.forEach(function (c) {
                         c.active = false;
@@ -293,7 +284,7 @@ NfRegistryService.prototype = {
         }
 
         for (var i = 0; i < this.dropletsSearchTerms.length; i++) {
-            newData = filterData(newData, this.dropletsSearchTerms[i], true, 
this.activeDropletColumn.name);
+            newData = this.filterData(newData, this.dropletsSearchTerms[i], 
true, sortBy);
         }
 
         newData = this.dataTableService.sortData(newData, sortBy, sortOrder);
@@ -302,7 +293,7 @@ NfRegistryService.prototype = {
     },
 
     /**
-     * Generates the auto complete options for the droplet filter.
+     * Generates the `autoCompleteDroplets` options for the droplet filter.
      */
     getAutoCompleteDroplets: function () {
         var self = this;
@@ -326,7 +317,7 @@ NfRegistryService.prototype = {
             case 'delete':
                 this.dialogService.openConfirm({
                     title: 'Delete Bucket',
-                    message: 'All versions of all items will be deleted.',
+                    message: 'All items stored in this bucket will be deleted 
as well.',
                     cancelButton: 'Cancel',
                     acceptButton: 'Delete',
                     acceptButtonColor: 'fds-warn'
@@ -334,31 +325,20 @@ NfRegistryService.prototype = {
                     function (accept) {
                         if (accept) {
                             
self.api.deleteBucket(bucket.identifier).subscribe(function (response) {
-                                if (response.link === null) {
-                                    self.buckets = 
self.buckets.filter(function (b) {
-                                        return b.identifier !== 
bucket.identifier;
-                                    });
-                                    var snackBarRef = 
self.snackBarService.openCoaster({
-                                        title: 'Success',
-                                        message: 'All versions of all items in 
this bucket, as well as the bucket, have been deleted.',
-                                        verticalPosition: 'bottom',
-                                        horizontalPosition: 'right',
-                                        icon: 'fa fa-check-circle-o',
-                                        color: '#1EB475',
-                                        duration: 3000
-                                    });
-                                    self.bucket = {};
-                                    self.filterBuckets();
-                                } else {
-                                    var snackBarRef = 
self.snackBarService.openCoaster({
-                                        title: 'Error',
-                                        message: 'Error deleting ' + 
bucket.name + '.',
-                                        verticalPosition: 'bottom',
-                                        horizontalPosition: 'right',
-                                        icon: 'fa fa-times-circle-o',
-                                        color: '#EF6162'
-                                    });
-                                }
+                                self.buckets = self.buckets.filter(function 
(b) {
+                                    return b.identifier !== bucket.identifier;
+                                });
+                                var snackBarRef = 
self.snackBarService.openCoaster({
+                                    title: 'Success',
+                                    message: 'All versions of all items in 
this bucket, as well as the bucket, have been deleted.',
+                                    verticalPosition: 'bottom',
+                                    horizontalPosition: 'right',
+                                    icon: 'fa fa-check-circle-o',
+                                    color: '#1EB475',
+                                    duration: 3000
+                                });
+                                self.bucket = {};
+                                self.filterBuckets();
                             });
                         }
                     });
@@ -372,7 +352,7 @@ NfRegistryService.prototype = {
     },
 
     /**
-     * Filter buckets.
+     * Filter buckets and sets the `isMultiBucketActionsDisabled` property 
accordingly.
      *
      * @param {string} sortBy       The column name to sort `bucketColumns` by.
      * @param {string} sortOrder    The order. Either 'ASC' or 'DES'
@@ -389,7 +369,6 @@ NfRegistryService.prototype = {
             for (var i = 0; i < arrayLength; i++) {
                 if (this.bucketColumns[i].sortable === true) {
                     sortBy = this.bucketColumns[i].name;
-                    this.activeBucketColumn = this.bucketColumns[i];
                     //only one column can be actively sorted so we reset all 
to inactive
                     this.bucketColumns.forEach(function (c) {
                         c.active = false;
@@ -405,7 +384,7 @@ NfRegistryService.prototype = {
         var newData = this.buckets;
 
         for (var i = 0; i < this.bucketsSearchTerms.length; i++) {
-            newData = filterData(newData, this.bucketsSearchTerms[i], true, 
this.activeBucketColumn.name);
+            newData = this.filterData(newData, this.bucketsSearchTerms[i], 
true, sortBy);
         }
 
         newData = this.dataTableService.sortData(newData, sortBy, sortOrder);
@@ -418,13 +397,13 @@ NfRegistryService.prototype = {
             }
         });
 
-        this.disableMultiBucketActions = (selected > 0) ? false : true;
+        this.isMultiBucketActionsDisabled = (selected > 0) ? false : true;
 
         this.getAutoCompleteBuckets();
     },
 
     /**
-     * Generates the auto complete options for the bucket filter.
+     * Generates the `autoCompleteBuckets` options for the bucket filter.
      */
     getAutoCompleteBuckets: function () {
         var self = this;
@@ -437,7 +416,7 @@ NfRegistryService.prototype = {
     },
 
     /**
-     * Sort `buckets` by `column`.
+     * Sort `filteredBuckets` by `column`.
      *
      * @param column    The column to sort by.
      */
@@ -446,7 +425,6 @@ NfRegistryService.prototype = {
             // toggle column sort order
             var sortOrder = column.sortOrder = (column.sortOrder === 'ASC') ? 
'DESC' : 'ASC';
             this.filterBuckets(column.name, sortOrder);
-            this.activeBucketsColumn = column;
             //only one column can be actively sorted so we reset all to 
inactive
             this.bucketColumns.forEach(function (c) {
                 c.active = false;
@@ -457,7 +435,8 @@ NfRegistryService.prototype = {
     },
 
     /**
-     * Returns true if each bucket in the `filteredBuckets` are selected.
+     * Returns true if each bucket in the `filteredBuckets` are selected and 
sets the `isMultiBucketActionsDisabled`
+     * property accordingly.
      *
      * @returns {boolean}
      */
@@ -473,12 +452,12 @@ NfRegistryService.prototype = {
             }
         });
 
-        this.disableMultiBucketActions = (selected > 0) ? false : true;
+        this.isMultiBucketActionsDisabled = (selected > 0) ? false : true;
         return allSelected;
     },
 
     /**
-     * Checks all filtered buckets checked state and sets the 
`allBucketsSelected`
+     * Checks each of the `filteredBuckets`'s `checked` property state and 
sets the `allBucketsSelected`
      * property accordingly.
      */
     determineAllBucketsSelectedState: function () {
@@ -502,23 +481,25 @@ NfRegistryService.prototype = {
     },
 
     /**
-     * Sets the `checked` property of each filtered bucket to true.
+     * Sets the `checked` property of each filtered bucket to true and sets
+     * the `isMultiBucketActionsDisabled` property accordingly.
      */
     selectAllBuckets: function () {
         this.filteredBuckets.forEach(function (c) {
             c.checked = true;
         });
-        this.disableMultiBucketActions = false;
+        this.isMultiBucketActionsDisabled = false;
     },
 
     /**
-     * Sets the `checked` property of each filtered bucket to false.
+     * Sets the `checked` property of each filtered bucket to false and sets
+     * the `isMultiBucketActionsDisabled` property accordingly.
      */
     deselectAllBuckets: function () {
         this.filteredBuckets.forEach(function (c) {
             c.checked = false;
         });
-        this.disableMultiBucketActions = true;
+        this.isMultiBucketActionsDisabled = true;
     },
 
     /**
@@ -533,6 +514,7 @@ NfRegistryService.prototype = {
             this.bucketsSearchTerms.splice(index, 1);
         }
         this.filterBuckets();
+        this.determineAllBucketsSelectedState();
     },
 
     /**
@@ -543,6 +525,7 @@ NfRegistryService.prototype = {
     bucketsSearchAdd: function (searchTerm) {
         this.bucketsSearchTerms.push(searchTerm);
         this.filterBuckets();
+        this.determineAllBucketsSelectedState();
     },
 
     /**
@@ -562,43 +545,37 @@ NfRegistryService.prototype = {
                     self.filteredBuckets.forEach(function (filteredBucket) {
                         if (filteredBucket.checked) {
                             
self.api.deleteBucket(filteredBucket.identifier).subscribe(function (response) {
-                                if (response.link === null) {
-                                    self.buckets = 
self.buckets.filter(function (bucket) {
-                                        return bucket.identifier !== 
filteredBucket.identifier;
-                                    });
-                                    var snackBarRef = 
self.snackBarService.openCoaster({
-                                        title: 'Success',
-                                        message: 'All versions of all items in 
' + filteredBucket.name + ' have been deleted.',
-                                        verticalPosition: 'bottom',
-                                        horizontalPosition: 'right',
-                                        icon: 'fa fa-check-circle-o',
-                                        color: '#1EB475',
-                                        duration: 3000
-                                    });
-                                    self.filterBuckets();
-                                    self.allBucketsSelected = false;
-                                } else {
-                                    var snackBarRef = 
self.snackBarService.openCoaster({
-                                        title: 'Error',
-                                        message: 'Error deleting ' + 
filteredBucket.name + '.',
-                                        verticalPosition: 'bottom',
-                                        horizontalPosition: 'right',
-                                        icon: 'fa fa-times-circle-o',
-                                        color: '#EF6162'
-                                    });
-                                }
+                                self.buckets = self.buckets.filter(function 
(bucket) {
+                                    return bucket.identifier !== 
filteredBucket.identifier;
+                                });
+                                var snackBarRef = 
self.snackBarService.openCoaster({
+                                    title: 'Success',
+                                    message: 'All versions of all items in ' + 
filteredBucket.name + ' have been deleted.',
+                                    verticalPosition: 'bottom',
+                                    horizontalPosition: 'right',
+                                    icon: 'fa fa-check-circle-o',
+                                    color: '#1EB475',
+                                    duration: 3000
+                                });
+                                self.filterBuckets();
                             });
                         }
                     });
+                    self.determineAllBucketsSelectedState();
                 }
             });
     },
 
-    sortUsers: function (sortEvent, column) {
+    /**
+     * Sort `users` and `groups` by `column`.
+     *
+     * @param column    The column to sort by.
+     */
+    sortUsersAndGroups: function (sortEvent, column) {
         if (column.sortable) {
             var sortBy = column.name;
             var sortOrder = column.sortOrder = (column.sortOrder === 'ASC') ? 
'DESC' : 'ASC';
-            this.filterUsers(sortBy, sortOrder);
+            this.filterUsersAndGroups(sortBy, sortOrder);
 
             //only one column can be actively sorted so we reset all to 
inactive
             this.userColumns.forEach(function (c) {
@@ -609,90 +586,384 @@ NfRegistryService.prototype = {
         }
     },
 
+    /**
+     * Loads the current user and updates the current user locally.
+     *
+     * @returns xhr
+     */
+    loadCurrentUser: function () {
+        var self = this;
+        // get the current user
+        return 
rxjs.Observable.of(this.api.loadCurrentUser().subscribe(function (currentUser) {
+                // if the user is logged, we want to determine if they were 
logged in using a certificate
+                if (currentUser.status !== "UNKNOWN") {
+                    // render the users name
+                    self.currentUser = currentUser;
+
+                    // render the logout button if there is a token locally
+                    if (self.nfStorage.getItem('jwt') !== null) {
+                        self.currentUser.canLogout = true;
+                    }
+                } else {
+                    // set the anonymous user label
+                    self.nfRegistryService.currentUser.identity = 'Anonymous';
+                }
+        }));
+    },
+
+    /**
+     * Adds a `searchTerm` to the `usersSearchTerms` and filters the `users` 
amd `groups`.
+     *
+     * @param {string} searchTerm   The search term to add.
+     */
     usersSearchRemove: function (searchTerm) {
         //only remove the first occurrence of the search term
         var index = this.usersSearchTerms.indexOf(searchTerm);
         if (index !== -1) {
             this.usersSearchTerms.splice(index, 1);
         }
-        this.filterUsers();
+        this.filterUsersAndGroups();
+        this.determineAllUsersAndGroupsSelectedState();
     },
 
+    /**
+     * Removes a `searchTerm` from the `usersSearchTerms` and filters the 
`users` amd `groups`.
+     *
+     * @param {string} searchTerm   The search term to remove.
+     */
     usersSearchAdd: function (searchTerm) {
         this.usersSearchTerms.push(searchTerm);
-        this.filterUsers();
+        this.filterUsersAndGroups();
+        this.determineAllUsersAndGroupsSelectedState();
     },
 
-    filterUsers: function (sortBy, sortOrder) {
-        if (this.allUsersSelected) {
-            this.toggleUsersSelectAll();
+    /**
+     * Filter users and groups.
+     *
+     * @param {string} [sortBy]       The column name to sort 
`userGroupsColumns` by.
+     * @param {string} [sortOrder]    The order. Either 'ASC' or 'DES'
+     */
+    filterUsersAndGroups: function (sortBy, sortOrder) {
+        // if `sortOrder` is `undefined` then use 'ASC'
+        if (sortOrder === undefined) {
+            sortOrder = 'ASC'
+        }
+        // if `sortBy` is `undefined` then find the first sortable column in 
`dropletColumns`
+        if (sortBy === undefined) {
+            var arrayLength = this.userColumns.length;
+            for (var i = 0; i < arrayLength; i++) {
+                if (this.userColumns[i].sortable === true) {
+                    sortBy = this.userColumns[i].name;
+                    //only one column can be actively sorted so we reset all 
to inactive
+                    this.userColumns.forEach(function (c) {
+                        c.active = false;
+                    });
+                    //and set this column as the actively sorted column
+                    this.userColumns[i].active = true;
+                    this.userColumns[i].sortOrder = sortOrder;
+                    break;
+                }
+            }
         }
-        this.deselectAllUsers();
-        var newData = this.users;
+
+        var newUsersData = this.users;
+        var newUserGroupsData = this.groups;
 
         for (var i = 0; i < this.usersSearchTerms.length; i++) {
-            newData = filterData(newData, this.usersSearchTerms[i], true);
+            newUsersData = this.filterData(newUsersData, 
this.usersSearchTerms[i], true);
         }
-        newData = this.dataTableService.sortData(newData, sortBy, sortOrder);
-        newData = this.dataTableService.pageData(newData, this.usersFromRow, 
this.usersCurrentPage * this.usersPageSize);
-        this.filteredUsers = newData;
-        this.getAutoCompleteUsers();
+
+        newUsersData = this.dataTableService.sortData(newUsersData, sortBy, 
sortOrder);
+        this.filteredUsers = newUsersData;
+
+        for (var i = 0; i < this.usersSearchTerms.length; i++) {
+            newUserGroupsData = this.filterData(newUserGroupsData, 
this.usersSearchTerms[i], true);
+        }
+
+        newUserGroupsData = this.dataTableService.sortData(newUserGroupsData, 
sortBy, sortOrder);
+        this.filteredUserGroups = newUserGroupsData;
+
+        this.getAutoCompleteUserAndGroups();
     },
 
-    toggleUserSelect: function (row) {
-        if (this.allFilteredUsersSelected()) {
-            this.allUsersSelected = true;
-        } else {
-            this.allUsersSelected = false;
+    /**
+     * Checks each of the `filteredUsers` and each of the `filteredUserGroups` 
`checked` property state and sets
+     * the `allUsersAndGroupsSelected`, `isMultiUserGroupActionsDisabled`, and 
the `isMultiUserActionsDisabled`
+     * properties accordingly.
+     */
+    determineAllUsersAndGroupsSelectedState: function () {
+        var selected = 0;
+        var allSelected = true;
+        this.isMultiUserGroupActionsDisabled = false;
+        this.filteredUserGroups.forEach(function (c) {
+            if (c.checked) {
+                selected++;
+            }
+            if (c.checked === undefined || c.checked === false) {
+                allSelected = false;
+            }
+        });
+
+        if (selected > 0) {
+            this.isMultiUserGroupActionsDisabled = true;
         }
+
+        this.filteredUsers.forEach(function (c) {
+            if (c.checked) {
+                selected++;
+            }
+            if (c.checked === undefined || c.checked === false) {
+                allSelected = false;
+            }
+        });
+        this.isMultiUserActionsDisabled = (selected > 0) ? false : true;
+        this.allUsersAndGroupsSelected = allSelected;
     },
 
+    /**
+     * Checks the `allUsersAndGroupsSelected` property state and either selects
+     * or deselects all `filteredUsers` and each `filteredUserGroups`.
+     */
     toggleUsersSelectAll: function () {
-        if (this.allUsersSelected) {
-            this.selectAllUsers();
+        if (this.allUsersAndGroupsSelected) {
+            this.selectAllUsersAndGroups();
         } else {
-            this.deselectAllUsers();
+            this.deselectAllUsersAndGroups();
         }
     },
 
-    selectAllUsers: function () {
+    /**
+     * Sets the `checked` property of each `filteredUsers` and each 
`filteredUserGroups` to true and sets
+     * the `allUsersAndGroupsSelected`, `isMultiUserGroupActionsDisabled`, and 
the `isMultiUserActionsDisabled`
+     * properties accordingly.
+     */
+    selectAllUsersAndGroups: function () {
         this.filteredUsers.forEach(function (c) {
             c.checked = true;
         });
+        this.filteredUserGroups.forEach(function (c) {
+            c.checked = true;
+        });
+        this.isMultiUserGroupActionsDisabled = (this.filteredUserGroups.length 
> 0) ? true : false;
+        this.allUsersAndGroupsSelected = true;
+        this.isMultiUserActionsDisabled = false;
     },
 
-    deselectAllUsers: function () {
+    /**
+     * Sets the `checked` property of each `filteredUsers` and each 
`filteredUserGroups` to false and sets
+     * the `allUsersAndGroupsSelected`, `isMultiUserGroupActionsDisabled`, and 
the `isMultiUserActionsDisabled`
+     * properties accordingly.
+     */
+    deselectAllUsersAndGroups: function () {
         this.filteredUsers.forEach(function (c) {
             c.checked = false;
         });
-    },
-    allFilteredUsersSelected: function () {
-        var allFilteredUsersSelected = true;
-        this.filteredUsers.forEach(function (c) {
-            if (c.checked === undefined || c.checked === false) {
-                allFilteredUsersSelected = false;
-            }
+        this.filteredUserGroups.forEach(function (c) {
+            c.checked = false;
         });
-
-        return allFilteredUsersSelected;
+        this.allUsersAndGroupsSelected = false;
+        this.isMultiUserGroupActionsDisabled = false;
+        this.isMultiUserActionsDisabled = true;
     },
 
-    getAutoCompleteUsers: function () {
+    /**
+     * Generates the `autoCompleteUsersAndGroups` options for the users and 
groups data table filter.
+     */
+    getAutoCompleteUserAndGroups: function () {
         var self = this;
-        this.autoCompleteUsers = [];
+        this.autoCompleteUsersAndGroups = [];
         this.userColumns.forEach(function (c) {
-            self.filteredUsers.forEach(function (r) {
-                (r[c.name.toLowerCase()]) ? 
self.autoCompleteUsers.push(r[c.name.toLowerCase()].toString()) : '';
+            var usersAndGroups = 
self.filteredUsers.concat(self.filteredUserGroups);
+            usersAndGroups.forEach(function (r) {
+                (r[c.name.toLowerCase()]) ? 
self.autoCompleteUsersAndGroups.push(r[c.name.toLowerCase()].toString()) : '';
             });
         });
     },
 
+    /**
+     * Execute the given user action.
+     *
+     * @param action        The action object.
+     * @param user          The user object the `action` will act upon.
+     */
+    executeUserAction: function (action, user) {
+        var self = this;
+        switch (action.name.toLowerCase()) {
+            case 'delete':
+                this.dialogService.openConfirm({
+                    title: 'Delete User',
+                    message: 'This user will lose all access to the registry.',
+                    cancelButton: 'Cancel',
+                    acceptButton: 'Delete',
+                    acceptButtonColor: 'fds-warn'
+                }).afterClosed().subscribe(
+                    function (accept) {
+                        if (accept) {
+                            
self.api.deleteUser(user.identifier).subscribe(function (response) {
+                                self.users = self.users.filter(function (u) {
+                                    return u.identifier !== user.identifier;
+                                });
+                                var snackBarRef = 
self.snackBarService.openCoaster({
+                                    title: 'Success',
+                                    message: 'User: ' + user.identity + ' has 
been deleted.',
+                                    verticalPosition: 'bottom',
+                                    horizontalPosition: 'right',
+                                    icon: 'fa fa-check-circle-o',
+                                    color: '#1EB475',
+                                    duration: 3000
+                                });
+                                self.filterUsersAndGroups();
+                            });
+                        }
+                    });
+                break;
+            case 'permissions':
+                
this.router.navigateByUrl('/nifi-registry/administration/users(' + action.type 
+ ':user/' + action.name + '/' + user.identifier + ')');
+                break;
+        }
+    },
+
+    /**
+     * Execute the given group action.
+     *
+     * @param action        The action object.
+     * @param group          The group object the `action` will act upon.
+     */
+    executeGroupAction: function (action, group) {
+        var self = this;
+        this.group = group;
+        switch (action.name.toLowerCase()) {
+            case 'delete':
+                this.dialogService.openConfirm({
+                    title: 'Delete Group',
+                    message: 'All policies granted to this group will be 
deleted as well.',
+                    cancelButton: 'Cancel',
+                    acceptButton: 'Delete',
+                    acceptButtonColor: 'fds-warn'
+                }).afterClosed().subscribe(
+                    function (accept) {
+                        if (accept) {
+                            
self.api.deleteUserGroup(group.identifier).subscribe(function (response) {
+                                self.groups = self.groups.filter(function (u) {
+                                    return u.identifier !== group.identifier;
+                                });
+                                var snackBarRef = 
self.snackBarService.openCoaster({
+                                    title: 'Success',
+                                    message: 'Group: ' + group.identity + ' 
has been deleted.',
+                                    verticalPosition: 'bottom',
+                                    horizontalPosition: 'right',
+                                    icon: 'fa fa-check-circle-o',
+                                    color: '#1EB475',
+                                    duration: 3000
+                                });
+                                self.filterUsersAndGroups();
+                            });
+                        }
+                    });
+                break;
+            case 'permissions':
+                
this.router.navigateByUrl('/nifi-registry/administration/users(' + action.type 
+ ':group/' + action.name + '/' + group.identifier + ')');
+                break;
+        }
+    },
+
+    /**
+     * Deletes all selected `filteredUserGroups` and `filteredUsers` and sets 
the `allUsersAndGroupsSelected`
+     * property accordingly.
+     */
+    deleteSelectedUsersAndGroups: function () {
+        var self = this;
+        this.dialogService.openConfirm({
+            title: 'Delete Users/Groups',
+            message: 'The selected users will lose all access to the registry 
and all policies granted to the selected groups will be deleted.',
+            cancelButton: 'Cancel',
+            acceptButton: 'Delete',
+            acceptButtonColor: 'fds-warn'
+        }).afterClosed().subscribe(
+            function (accept) {
+                if (accept) {
+                    self.filteredUserGroups.forEach(function 
(filteredUserGroup) {
+                        if (filteredUserGroup.checked) {
+                            
self.api.deleteUserGroup(filteredUserGroup.identifier).subscribe(function 
(response) {
+                                self.groups = self.groups.filter(function (u) {
+                                    return u.identifier !== 
filteredUserGroup.identifier;
+                                });
+                                var snackBarRef = 
self.snackBarService.openCoaster({
+                                    title: 'Success',
+                                    message: 'User group: ' + 
filteredUserGroup.identity + ' has been deleted.',
+                                    verticalPosition: 'bottom',
+                                    horizontalPosition: 'right',
+                                    icon: 'fa fa-check-circle-o',
+                                    color: '#1EB475',
+                                    duration: 3000
+                                });
+                                self.filterUsersAndGroups();
+                            });
+                        }
+                    });
+                    self.filteredUsers.forEach(function (filteredUser) {
+                        if (filteredUser.checked) {
+                            
self.api.deleteUser(filteredUser.identifier).subscribe(function (response) {
+                                self.users = self.users.filter(function (u) {
+                                    return u.identifier !== 
filteredUser.identifier;
+                                });
+                                var snackBarRef = 
self.snackBarService.openCoaster({
+                                    title: 'Success',
+                                    message: 'User: ' + filteredUser.identity 
+ ' has been deleted.',
+                                    verticalPosition: 'bottom',
+                                    horizontalPosition: 'right',
+                                    icon: 'fa fa-check-circle-o',
+                                    color: '#1EB475',
+                                    duration: 3000
+                                });
+                                self.filterUsersAndGroups();
+                            });
+                        }
+                    });
+                    self.determineAllUsersAndGroupsSelectedState();
+                }
+            });
+    },
+
+    /**
+     * Utility method that performs the custom search capability for data 
tables.
+     *
+     * @param data          The data to search.
+     * @param searchTerm    The term we are looking for.
+     * @param ignoreCase    Ignore case.
+     * @returns {*}
+     */
+    filterData: function(data, searchTerm, ignoreCase) {
+        var field = '';
+        if (searchTerm.indexOf(":") > -1) {
+            field = searchTerm.split(':')[0].trim();
+            searchTerm = searchTerm.split(':')[1].trim();
+        }
+        var filter = searchTerm ? (ignoreCase ? searchTerm.toLowerCase() : 
searchTerm) : '';
+
+        if (filter) {
+            data = data.filter(function (item) {
+                var res = Object.keys(item).find(function (key) {
+                    if (key !== field && field !== '') {
+                        return false;
+                    }
+                    var preItemValue = ('' + item[key]);
+                    var itemValue = ignoreCase ? preItemValue.toLowerCase() : 
preItemValue;
+                    return itemValue.indexOf(filter) > -1;
+                });
+                return !(typeof res === 'undefined');
+            });
+        }
+        return data;
+    }
+
     //</editor-fold>
 };
 
 NfRegistryService.parameters = [
-    covalentCore.TdDataTableService,
     NfRegistryApi,
+    NfStorage,
+    covalentCore.TdDataTableService,
     ngRouter.Router,
     fdsDialogsModule.FdsDialogService,
     fdsSnackBarsModule.FdsSnackBarService

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/247985ad/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js 
b/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js
index bd3f898..2b9d92f 100644
--- a/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js
+++ b/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.spec.js
@@ -17,6 +17,7 @@
 
 var NfRegistryRoutes = require('nifi-registry/nf-registry.routes.js');
 var ngCoreTesting = require('@angular/core/testing');
+var ngCommonHttpTesting = require('@angular/common/http/testing');
 var ngHttpTesting = require('@angular/http/testing');
 var ngCommon = require('@angular/common');
 var FdsDemo = 
require('nifi-registry/components/fluid-design-system/fds-demo.js');
@@ -27,9 +28,10 @@ var NfPageNotFoundComponent = 
require('nifi-registry/components/page-not-found/n
 var NfRegistryExplorer = 
require('nifi-registry/components/explorer/nf-registry-explorer.js');
 var NfRegistryAdministration = 
require('nifi-registry/components/administration/nf-registry-administration.js');
 var NfRegistryUsersAdministration = 
require('nifi-registry/components/administration/users/nf-registry-users-administration.js');
-var NfRegistryAddUser = 
require('nifi-registry/components/administration/users/add/nf-registry-add-user.js');
+var NfRegistryAddUser = 
require('nifi-registry/components/administration/users/dialogs/add-user/nf-registry-add-user.js');
 var NfRegistryUserDetails = 
require('nifi-registry/components/administration/users/details/nf-registry-user-details.js');
 var NfRegistryUserPermissions = 
require('nifi-registry/components/administration/users/permissions/nf-registry-user-permissions.js');
+var NfRegistryUserGroupPermissions = 
require('nifi-registry/components/administration/user-group/permissions/nf-registry-user-group-permissions.js');
 var NfRegistryBucketPermissions = 
require('nifi-registry/components/administration/workflow/buckets/permissions/nf-registry-bucket-permissions.js');
 var NfRegistryWorkflowAdministration = 
require('nifi-registry/components/administration/workflow/nf-registry-workflow-administration.js');
 var NfRegistryGridListViewer = 
require('nifi-registry/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.js');
@@ -41,6 +43,10 @@ var ngHttp = require('@angular/http');
 var rxjs = require('rxjs/Rx');
 var fdsDialogsModule = require('@fluid-design-system/dialogs');
 var ngRouter = require('@angular/router');
+var ngCommonHttp = require('@angular/common/http');
+var NfRegistryTokenInterceptor = 
require('nifi-registry/services/nf-registry.token.interceptor.js');
+var NfRegistryAuthService = 
require('nifi-registry/services/nf-registry.auth.service.js');
+var NfStorage = require('nifi-registry/services/nf-storage.service.js');
 
 describe('NfRegistry Service isolated unit tests', function () {
     var comp;
@@ -182,7 +188,6 @@ describe('NfRegistry Service isolated unit tests', function 
() {
         var filterBucketsCall = nfRegistryService.filterBuckets.calls.first();
         expect(filterBucketsCall.args[0]).toBe('name');
         expect(filterBucketsCall.args[1]).toBe('ASC');
-        expect(nfRegistryService.activeBucketsColumn).toBe(column);
     });
 
     it('should generate the auto complete options for the droplet filter.', 
function () {
@@ -241,7 +246,7 @@ describe('NfRegistry Service isolated unit tests', function 
() {
 
         //assertions
         expect(allSelected).toBe(false);
-        expect(nfRegistryService.disableMultiBucketActions).toBe(true);
+        expect(nfRegistryService.isMultiBucketActionsDisabled).toBe(true);
     });
 
     it('should check if all buckets are selected and return true.', function 
() {
@@ -261,7 +266,7 @@ describe('NfRegistry Service isolated unit tests', function 
() {
 
         //assertions
         expect(allSelected).toBe(true);
-        expect(nfRegistryService.disableMultiBucketActions).toBe(false);
+        expect(nfRegistryService.isMultiBucketActionsDisabled).toBe(false);
     });
 
     it('should set the `allBucketsSelected` state to true.', function () {
@@ -324,7 +329,7 @@ describe('NfRegistry Service isolated unit tests', function 
() {
 
         //assertions
         expect(nfRegistryService.filteredBuckets[0].checked).toBe(true);
-        expect(nfRegistryService.disableMultiBucketActions).toBe(false);
+        expect(nfRegistryService.isMultiBucketActionsDisabled).toBe(false);
     });
 
     it('should deselect all buckets.', function () {
@@ -335,7 +340,7 @@ describe('NfRegistry Service isolated unit tests', function 
() {
 
         //assertions
         expect(nfRegistryService.filteredBuckets[0].checked).toBe(false);
-        expect(nfRegistryService.disableMultiBucketActions).toBe(true);
+        expect(nfRegistryService.isMultiBucketActionsDisabled).toBe(true);
     });
 
     it('should add a bucket search term.', function () {
@@ -373,6 +378,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
     var comp;
     var fixture;
     var nfRegistryService;
+    var nfRegistryApi;
 
     beforeEach(function () {
         ngCoreTesting.TestBed.configureTestingModule({
@@ -380,8 +386,10 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
                 ngMoment.MomentModule,
                 ngHttp.HttpModule,
                 ngHttp.JsonpModule,
+                ngCommonHttp.HttpClientModule,
                 fdsCore,
-                NfRegistryRoutes
+                NfRegistryRoutes,
+                ngCommonHttpTesting.HttpClientTestingModule
             ],
             declarations: [
                 FdsDemo,
@@ -391,6 +399,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
                 NfRegistryUsersAdministration,
                 NfRegistryUserDetails,
                 NfRegistryUserPermissions,
+                NfRegistryUserGroupPermissions,
                 NfRegistryBucketPermissions,
                 NfRegistryAddUser,
                 NfRegistryWorkflowAdministration,
@@ -401,7 +410,14 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
             ],
             providers: [
                 NfRegistryService,
+                NfRegistryAuthService,
                 NfRegistryApi,
+                NfStorage,
+                {
+                    provide: ngCommonHttp.HTTP_INTERCEPTORS,
+                    useClass: NfRegistryTokenInterceptor,
+                    multi: true
+                },
                 {
                     provide: ngCommon.APP_BASE_HREF,
                     useValue: '/'
@@ -426,12 +442,15 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
         fixture.detectChanges();
 
         // Spy
-        spyOn(nfRegistryApi.http, 'get').and.callThrough();
+        spyOn(nfRegistryApi.http, 'get').and.callFake(function () {});
+        spyOn(nfRegistryApi.http, 'post').and.callFake(function () {});
+        spyOn(nfRegistryApi, 'ticketExchange').and.callFake(function () 
{}).and.returnValue(rxjs.Observable.of({}));
+        spyOn(nfRegistryService, 'loadCurrentUser').and.callFake(function () 
{}).and.returnValue(rxjs.Observable.of({}));
     });
 
     it('should retrieve the snapshot metadata for the given droplet.', 
ngCoreTesting.fakeAsync(function () {
         //Spy
-        spyOn(nfRegistryService.api, 
'getDropletSnapshotMetadata').and.callFake(function () {
+        spyOn(nfRegistryApi, 
'getDropletSnapshotMetadata').and.callFake(function () {
         }).and.returnValue(rxjs.Observable.of([{
             version: 999
         }]));
@@ -450,9 +469,9 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
 
         //assertions
         expect(droplet.snapshotMetadata[0].version).toBe(999);
-        
expect(nfRegistryService.api.getDropletSnapshotMetadata).toHaveBeenCalled();
-        
expect(nfRegistryService.api.getDropletSnapshotMetadata.calls.count()).toBe(1);
-        var getDropletSnapshotMetadataCall = 
nfRegistryService.api.getDropletSnapshotMetadata.calls.first()
+        expect(nfRegistryApi.getDropletSnapshotMetadata).toHaveBeenCalled();
+        expect(nfRegistryApi.getDropletSnapshotMetadata.calls.count()).toBe(1);
+        var getDropletSnapshotMetadataCall = 
nfRegistryApi.getDropletSnapshotMetadata.calls.first()
         expect(getDropletSnapshotMetadataCall.args[0]).toBe('test/id');
         expect(getDropletSnapshotMetadataCall.args[1]).toBe(true);
     }));
@@ -467,7 +486,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
                 return rxjs.Observable.of(true);
             }
         });
-        spyOn(nfRegistryService.api, 'deleteDroplet').and.callFake(function () 
{
+        spyOn(nfRegistryApi, 'deleteDroplet').and.callFake(function () {
         }).and.returnValue(rxjs.Observable.of({identifier: 
'2e04b4fb-9513-47bb-aa74-1ae34616bfdc', link: null}));
         spyOn(nfRegistryService, 'filterDroplets').and.callFake(function () {
         });
@@ -479,7 +498,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
             link: {href: 'testhref'}
         });
 
-        // wait for async nfRegistryService.api.deleteDroplet call
+        // wait for async nfRegistryApi.deleteDroplet call
         ngCoreTesting.tick();
 
         //inform angular to detect changes
@@ -490,7 +509,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
         expect(nfRegistryService.filterDroplets).toHaveBeenCalled();
         var openConfirmCall = 
nfRegistryService.dialogService.openConfirm.calls.first()
         expect(openConfirmCall.args[0].title).toBe('Delete testtype');
-        var deleteDropletCall = 
nfRegistryService.api.deleteDroplet.calls.first()
+        var deleteDropletCall = nfRegistryApi.deleteDroplet.calls.first()
         expect(deleteDropletCall.args[0]).toBe('testhref');
     }));
 
@@ -535,7 +554,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
         // The function to test
         nfRegistryService.filterDroplets();
 
-        // wait for async nfRegistryService.api.deleteDroplet call
+        // wait for async nfRegistryApi.deleteDroplet call
         ngCoreTesting.tick();
 
         //inform angular to detect changes
@@ -588,7 +607,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
         // The function to test
         nfRegistryService.filterDroplets();
 
-        // wait for async nfRegistryService.api.deleteDroplet call
+        // wait for async nfRegistryApi.deleteDroplet call
         ngCoreTesting.tick();
 
         //inform angular to detect changes
@@ -613,7 +632,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
                 return rxjs.Observable.of(true);
             }
         });
-        spyOn(nfRegistryService.api, 'deleteBucket').and.callFake(function () {
+        spyOn(nfRegistryApi, 'deleteBucket').and.callFake(function () {
         }).and.returnValue(rxjs.Observable.of({identifier: 
'2e04b4fb-9513-47bb-aa74-1ae34616bfdc', link: null}));
 
         // object to be updated by the test
@@ -639,7 +658,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
 
         //assertions
         expect(dialogService.openConfirm).toHaveBeenCalled();
-        expect(nfRegistryService.api.deleteBucket).toHaveBeenCalled();
+        expect(nfRegistryApi.deleteBucket).toHaveBeenCalled();
         expect(nfRegistryService.filterBuckets).toHaveBeenCalled();
         expect(nfRegistryService.buckets.length).toBe(1);
         expect(nfRegistryService.buckets[0].identifier).toBe(1);
@@ -683,12 +702,12 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
         spyOn(nfRegistryService, 'getAutoCompleteBuckets');
 
         //assertion
-        expect(nfRegistryService.disableMultiBucketActions).toBe(true);
+        expect(nfRegistryService.isMultiBucketActionsDisabled).toBe(true);
 
         // The function to test
         nfRegistryService.filterBuckets();
 
-        // wait for async nfRegistryService.api.deleteDroplet call
+        // wait for async nfRegistryApi.deleteDroplet call
         ngCoreTesting.tick();
 
         //inform angular to detect changes
@@ -698,7 +717,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
         expect(nfRegistryService.filteredBuckets.length).toBe(1);
         expect(nfRegistryService.filteredBuckets[0].name).toBe('Bucket #1');
         expect(nfRegistryService.getAutoCompleteBuckets).toHaveBeenCalled();
-        expect(nfRegistryService.disableMultiBucketActions).toBe(false);
+        expect(nfRegistryService.isMultiBucketActionsDisabled).toBe(false);
     }));
 
     it('should delete all selected buckets.', ngCoreTesting.fakeAsync(function 
() {
@@ -716,7 +735,7 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
                 return rxjs.Observable.of(true);
             }
         });
-        spyOn(nfRegistryService.api, 'deleteBucket').and.callFake(function () {
+        spyOn(nfRegistryApi, 'deleteBucket').and.callFake(function () {
         }).and.returnValue(rxjs.Observable.of({identifier: 999, link: null}));
 
         // object to be updated by the test
@@ -742,11 +761,11 @@ describe('NfRegistry Service w/ Angular testing utils', 
function () {
 
         //assertions
         expect(dialogService.openConfirm).toHaveBeenCalled();
-        expect(nfRegistryService.api.deleteBucket).toHaveBeenCalled();
-        expect(nfRegistryService.api.deleteBucket.calls.count()).toBe(1);
+        expect(nfRegistryApi.deleteBucket).toHaveBeenCalled();
+        expect(nfRegistryApi.deleteBucket.calls.count()).toBe(1);
         expect(nfRegistryService.filterBuckets).toHaveBeenCalled();
         expect(nfRegistryService.filterBuckets.calls.count()).toBe(1);
-        expect(nfRegistryService.disableMultiBucketActions).toBe(true);
+        expect(nfRegistryService.isMultiBucketActionsDisabled).toBe(true);
         expect(nfRegistryService.allBucketsSelected).toBe(false);
         expect(nfRegistryService.buckets.length).toBe(1);
         expect(nfRegistryService.buckets[0].identifier).toBe(1);

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/247985ad/nifi-registry-web-ui/src/main/webapp/services/nf-registry.token.interceptor.js
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-ui/src/main/webapp/services/nf-registry.token.interceptor.js
 
b/nifi-registry-web-ui/src/main/webapp/services/nf-registry.token.interceptor.js
new file mode 100644
index 0000000..e1abb95
--- /dev/null
+++ 
b/nifi-registry-web-ui/src/main/webapp/services/nf-registry.token.interceptor.js
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+var NfRegistryAuthService = 
require('nifi-registry/services/nf-registry.auth.service.js');
+
+/**
+ * NfRegistryTokenInterceptor constructor.
+ * @constructor
+ */
+function NfRegistryTokenInterceptor(nfRegistryAuthService) {
+    this.auth = nfRegistryAuthService;
+};
+
+NfRegistryTokenInterceptor.prototype = {
+    constructor: NfRegistryTokenInterceptor,
+
+    /**
+     * Generates the droplet grid-list explorer component's sorting menu 
options.
+     *
+     * @param request       angular HttpRequest.
+     * @param next          angular HttpHandler.
+     * @returns {Observable HTTPEvent}
+     */
+    intercept: function(request, next) {
+        var token = this.auth.getToken();
+        if(token) {
+            request = request.clone({headers: 
request.headers.set('Authorization', 'Bearer ' + token)});
+        }
+        return next.handle(request);
+    }
+};
+
+NfRegistryTokenInterceptor.parameters = [NfRegistryAuthService];
+
+module.exports = NfRegistryTokenInterceptor;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/247985ad/nifi-registry-web-ui/src/main/webapp/services/nf-storage.service.js
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-ui/src/main/webapp/services/nf-storage.service.js 
b/nifi-registry-web-ui/src/main/webapp/services/nf-storage.service.js
new file mode 100644
index 0000000..f1045b9
--- /dev/null
+++ b/nifi-registry-web-ui/src/main/webapp/services/nf-storage.service.js
@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+
+// Store items for two days before being eligible for removal.
+var MILLIS_PER_DAY = 86400000;
+var TWO_DAYS = MILLIS_PER_DAY * 2;
+
+var isUndefined = function (obj) {
+    return typeof obj === 'undefined';
+};
+
+var isNull = function (obj) {
+    return obj === null;
+};
+
+var isDefinedAndNotNull = function (obj) {
+    return !isUndefined(obj) && !isNull(obj);
+};
+
+/**
+ * Checks the expiration for the specified entry.
+ *
+ * @param {object} entry
+ * @returns {boolean}
+ */
+var checkExpiration = function (entry) {
+    if (isDefinedAndNotNull(entry.expires)) {
+        // get the expiration
+        var expires = new Date(entry.expires);
+        var now = new Date();
+
+        // return whether the expiration date has passed
+        return expires.valueOf() < now.valueOf();
+    } else {
+        return false;
+    }
+};
+
+/**
+ * Gets an enty for the key. The entry expiration is not checked.
+ *
+ * @param {string} key
+ */
+var getEntry = function (key) {
+    try {
+        // parse the entry
+        var entry = JSON.parse(localStorage.getItem(key));
+
+        // ensure the entry and item are present
+        if (isDefinedAndNotNull(entry)) {
+            return entry;
+        } else {
+            return null;
+        }
+    } catch (e) {
+        return null;
+    }
+};
+
+/**
+ * NfRegistryStorage constructor.
+ * @constructor
+ */
+function NfRegistryStorage() {
+};
+
+NfRegistryStorage.prototype = {
+    constructor: NfRegistryStorage,
+    /**
+     * Initializes the storage. Items will be persisted for two days. Once the 
scripts runs
+     * thereafter, all eligible items will be removed. This strategy does not 
support persistence.
+     */
+    init: function () {
+        for (var i = 0; i < localStorage.length; i++) {
+            try {
+                // get the next item
+                var key = localStorage.key(i);
+
+                // attempt to get the item which will expire if necessary
+                this.getItem(key);
+            } catch (e) {
+            }
+        }
+    },
+
+    /**
+     * Stores the specified item.
+     *
+     * @param {string} key
+     * @param {object} item
+     * @param {integer} expires
+     */
+    setItem: function (key, item, expires) {
+        // calculate the expiration
+        expires = isDefinedAndNotNull(expires) ? expires : new 
Date().valueOf() + TWO_DAYS;
+
+        // create the entry
+        var entry = {
+            expires: expires,
+            item: item
+        };
+
+        // store the item
+        localStorage.setItem(key, JSON.stringify(entry));
+    },
+
+    /**
+     * Returns whether there is an entry for this key. This will not check the 
expiration. If
+     * the entry is expired, it will return null on a subsequent getItem 
invocation.
+     *
+     * @param {string} key
+     * @returns {boolean}
+     */
+    hasItem: function (key) {
+        return getEntry(key) !== null;
+    },
+
+    /**
+     * Gets the item with the specified key. If an item with this key does
+     * not exist, null is returned. If an item exists but cannot be parsed
+     * or is malformed/unrecognized, null is returned.
+     *
+     * @param {type} key
+     */
+    getItem: function (key) {
+        var entry = getEntry(key);
+        if (entry === null) {
+            return null;
+        }
+
+        // if the entry is expired, drop it and return null
+        if (checkExpiration(entry)) {
+            this.removeItem(key);
+            return null;
+        }
+
+        // if the entry has the specified field return its value
+        if (isDefinedAndNotNull(entry['item'])) {
+            return entry['item'];
+        } else {
+            return null;
+        }
+    },
+
+    /**
+     * Gets the expiration for the specified item. This will not check the 
expiration. If
+     * the entry is expired, it will return null on a subsequent getItem 
invocation.
+     *
+     * @param {string} key
+     * @returns {integer}
+     */
+    getItemExpiration: function (key) {
+        var entry = getEntry(key);
+        if (entry === null) {
+            return null;
+        }
+
+        // if the entry has the specified field return its value
+        if (isDefinedAndNotNull(entry['expires'])) {
+            return entry['expires'];
+        } else {
+            return null;
+        }
+    },
+
+    /**
+     * Extracts the subject from the specified jwt. If the jwt is not as 
expected
+     * an empty string is returned.
+     *
+     * @param {string} jwt
+     * @returns {string}
+     */
+    getJwtPayload: function (jwt) {
+        if (isDefinedAndNotNull(jwt)) {
+            var segments = jwt.split(/\./);
+            if (segments.length !== 3) {
+                return '';
+            }
+
+            var rawPayload = window.atob(segments[1]);
+            var payload = JSON.parse(rawPayload);
+
+            if (isDefinedAndNotNull(payload)) {
+                return payload;
+            } else {
+                return null;
+            }
+        }
+
+        return null;
+    },
+
+    /**
+     * Removes the item with the specified key.
+     *
+     * @param {type} key
+     */
+    removeItem: function (key) {
+        localStorage.removeItem(key);
+    }
+};
+
+NfRegistryStorage.parameters = [];
+
+module.exports = NfRegistryStorage;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/247985ad/nifi-registry-web-ui/src/main/webapp/systemjs.builder.config.js
----------------------------------------------------------------------
diff --git a/nifi-registry-web-ui/src/main/webapp/systemjs.builder.config.js 
b/nifi-registry-web-ui/src/main/webapp/systemjs.builder.config.js
index a32bb3e..fa874e2 100644
--- a/nifi-registry-web-ui/src/main/webapp/systemjs.builder.config.js
+++ b/nifi-registry-web-ui/src/main/webapp/systemjs.builder.config.js
@@ -33,6 +33,7 @@
             // Angular
             '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
             '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
+            '@angular/common/http': 
'npm:@angular/common/bundles/common-http.umd.js',
             '@angular/platform-browser': 
'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
             '@angular/platform-browser-dynamic': 
'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
             '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
@@ -74,6 +75,7 @@
             'core-js': 'npm:core-js/client/shim.min.js',
             'superagent': 'npm:superagent/superagent.js',
             'querystring': 'npm:querystring',
+            'tslib': 'npm:tslib/tslib.js',
 
             // Fluid Design System
             '@fluid-design-system/core': 
'npm:@fluid-design-system/dist/platform/core/fluid-design-system.module.js',
@@ -94,13 +96,19 @@
             'nifi-registry/nf-registry.js': 'nf-registry.js',
             'nifi-registry/services/nf-registry.api.js': 
'services/nf-registry.api.js',
             'nifi-registry/services/nf-registry.service.js': 
'services/nf-registry.service.js',
+            'nifi-registry/services/nf-storage.service.js': 
'services/nf-storage.service.js',
+            'nifi-registry/services/nf-registry.auth.service.js': 
'services/nf-registry.auth.service.js',
+            'nifi-registry/services/nf-registry.token.interceptor.js': 
'services/nf-registry.token.interceptor.js',
             
'nifi-registry/components/page-not-found/nf-registry-page-not-found.js': 
'components/page-not-found/nf-registry-page-not-found.js',
             'nifi-registry/components/explorer/nf-registry-explorer.js': 
'components/explorer/nf-registry-explorer.js',
             
'nifi-registry/components/administration/nf-registry-administration.js': 
'components/administration/nf-registry-administration.js',
             
'nifi-registry/components/administration/users/nf-registry-users-administration.js':
 'components/administration/users/nf-registry-users-administration.js',
-            
'nifi-registry/components/administration/users/add/nf-registry-add-user.js': 
'components/administration/users/add/nf-registry-add-user.js',
+            
'nifi-registry/components/administration/users/dialogs/add-user/nf-registry-add-user.js':
 'components/administration/users/dialogs/add-user/nf-registry-add-user.js',
+            
'nifi-registry/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.js':
 
'components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.js',
+            
'nifi-registry/components/administration/users/dialogs/add-selected-users-to-group/nf-registry-add-selected-users-to-group.js':
 
'components/administration/users/dialogs/add-selected-users-to-group/nf-registry-add-selected-users-to-group.js',
             
'nifi-registry/components/administration/users/details/nf-registry-user-details.js':
 'components/administration/users/details/nf-registry-user-details.js',
             
'nifi-registry/components/administration/users/permissions/nf-registry-user-permissions.js':
 'components/administration/users/permissions/nf-registry-user-permissions.js',
+            
'nifi-registry/components/administration/user-group/permissions/nf-registry-user-group-permissions.js':
 
'components/administration/user-group/permissions/nf-registry-user-group-permissions.js',
             
'nifi-registry/components/administration/workflow/dialogs/nf-registry-create-bucket.js':
 'components/administration/workflow/dialogs/nf-registry-create-bucket.js',
             
'nifi-registry/components/administration/workflow/buckets/permissions/nf-registry-bucket-permissions.js':
 
'components/administration/workflow/buckets/permissions/nf-registry-bucket-permissions.js',
             
'nifi-registry/components/administration/workflow/nf-registry-workflow-administration.js':
 'components/administration/workflow/nf-registry-workflow-administration.js',

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/247985ad/nifi-registry-web-ui/src/main/webapp/systemjs.spec.config.js
----------------------------------------------------------------------
diff --git a/nifi-registry-web-ui/src/main/webapp/systemjs.spec.config.js 
b/nifi-registry-web-ui/src/main/webapp/systemjs.spec.config.js
index fd40882..fead0bc 100644
--- a/nifi-registry-web-ui/src/main/webapp/systemjs.spec.config.js
+++ b/nifi-registry-web-ui/src/main/webapp/systemjs.spec.config.js
@@ -32,6 +32,8 @@
             // Angular
             '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
             '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
+            '@angular/common/http': 
'npm:@angular/common/bundles/common-http.umd.js',
+            '@angular/common/http/testing': 
'npm:@angular/common/bundles/common-http-testing.umd.js',
             '@angular/platform-browser': 
'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
             '@angular/platform-browser-dynamic': 
'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
             '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
@@ -73,6 +75,7 @@
             'core-js': 'npm:core-js/client/shim.min.js',
             'superagent': 'npm:superagent/superagent.js',
             'querystring': 'npm:querystring',
+            'tslib': 'npm:tslib/tslib.js',
 
             // Fluid Design System
             '@fluid-design-system/core': 
'npm:@fluid-design-system/dist/platform/core/fluid-design-system.module.js',

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/247985ad/nifi-registry-web-ui/src/main/webapp/theming/_structureElements.scss
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-ui/src/main/webapp/theming/_structureElements.scss 
b/nifi-registry-web-ui/src/main/webapp/theming/_structureElements.scss
index 5e9a1c2..b8f8978 100644
--- a/nifi-registry-web-ui/src/main/webapp/theming/_structureElements.scss
+++ b/nifi-registry-web-ui/src/main/webapp/theming/_structureElements.scss
@@ -16,7 +16,8 @@
  */
 
 body {
-    background: $grey5;
+    background: $grey5 url('/nifi-registry/images/registry-logo-web-app.svg') 
no-repeat center center;
+    background-size: 40%;
 }
 
 #nf-registry-app-container {
@@ -70,10 +71,35 @@ body {
     left: 0px;
     right: 0px;
     bottom: 0px;
-    background: $grey5;
     min-height: 370px;
     min-width: 1045px;
     overflow: auto;
+    background: $grey5 url('/nifi-registry/images/registry-logo-web-app.svg') 
no-repeat center center;
+    background-size: 40%;
+}
+
+#current-user {
+    font-family: 'Roboto Slab';
+    font-style: normal;
+    font-weight: normal;
+    font-size: 12px;
+    max-width: 250px;
+    text-overflow: ellipsis;
+    line-height: normal;
+    overflow: hidden;
+    white-space: nowrap;
+    color: $grey5;
+}
+
+#logout-link-container {
+    font-size: 12px;
+    text-transform:uppercase;
+}
+
+#loading-spinner {
+    position: absolute;
+    top: calc(50% - 50px);
+    left: calc(50% - 50px);
 }
 
 mat-sidenav {

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/247985ad/nifi-registry-web-ui/src/main/webapp/theming/components/administration/users/_structureElements.scss
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-ui/src/main/webapp/theming/components/administration/users/_structureElements.scss
 
b/nifi-registry-web-ui/src/main/webapp/theming/components/administration/users/_structureElements.scss
index 0c41820..0d5c4ad 100644
--- 
a/nifi-registry-web-ui/src/main/webapp/theming/components/administration/users/_structureElements.scss
+++ 
b/nifi-registry-web-ui/src/main/webapp/theming/components/administration/users/_structureElements.scss
@@ -47,7 +47,7 @@
     right: 10px;
 }
 
-#nf-registry-add-user-side-nav-container {
+#nf-registry-user-group-permissions-side-nav-container {
     position: absolute;
     bottom: 15px;
     right: 10px;
@@ -59,3 +59,8 @@
     right: 10px;
 }
 
+#nifi-registry-add-selected-users-to-group-list-container {
+    width: 100%;
+    overflow: auto;
+    max-height: 245px;
+}

Reply via email to