You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2020/12/30 19:35:23 UTC

[trafficcontrol] branch master updated: TP: adds tenant tree to replace flat, hard to visualize tenants table (#5393)

This is an automated email from the ASF dual-hosted git repository.

ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 9a06d61  TP: adds tenant tree to replace flat, hard to visualize tenants table (#5393)
9a06d61 is described below

commit 9a06d615856fcea881f445d5b7e266b993601a0d
Author: Jeremy Mitchell <mi...@users.noreply.github.com>
AuthorDate: Wed Dec 30 12:35:12 2020 -0700

    TP: adds tenant tree to replace flat, hard to visualize tenants table (#5393)
    
    * adds tenant tree instead of table
    
    * adds the ability to export the tenant info as a CSV file
    
    * adds tests for tenants view
    
    * adds a changelog entry
    
    * adds action buttons to each tenant item
    
    * prepopulates the parent when creating a child tenant
    
    * for a tenant, provides the ability to view ALL delivery services accessible to the tenant OR just the ones directly assigned to the tenant.
    
    * fixes success messages when deleting a tenant
    
    * only show all ds's if value is true
    
    * removes drag cursor from tenant tree
    
    * adds missing carriage return
    
    * adds tenant tests to gha
---
 .github/actions/tp-e2e-tests/conf.json             |  3 +-
 CHANGELOG.md                                       |  1 +
 traffic_portal/app/src/common/api/TenantService.js |  1 -
 .../modules/form/tenant/FormTenantController.js    |  4 --
 .../form/tenant/edit/FormEditTenantController.js   |  7 ++-
 .../modules/form/tenant/form.tenant.tpl.html       | 13 ++++-
 .../common/modules/form/user/FormUserController.js |  4 --
 .../common/modules/form/user/form.user.tpl.html    |  2 +-
 .../table/tenants/TableTenantsController.js        | 64 +++++++++++++++-------
 .../modules/table/tenants/table.tenants.tpl.html   | 61 ++++++++++++++-------
 .../app/src/common/service/utils/FileUtils.js      | 50 +++++++++++++++++
 .../private/tenants/deliveryServices/index.js      |  9 ++-
 .../app/src/modules/private/tenants/new/index.js   |  7 ++-
 traffic_portal/app/src/styles/main.scss            |  6 ++
 traffic_portal/test/end_to_end/conf.json           |  3 +-
 .../end_to_end/tenants/pageData.js}                | 22 ++------
 .../test/end_to_end/tenants/tenants-spec.js        | 55 +++++++++++++++++++
 17 files changed, 235 insertions(+), 77 deletions(-)

diff --git a/.github/actions/tp-e2e-tests/conf.json b/.github/actions/tp-e2e-tests/conf.json
index 375ba53..6301c2d 100644
--- a/.github/actions/tp-e2e-tests/conf.json
+++ b/.github/actions/tp-e2e-tests/conf.json
@@ -30,7 +30,8 @@
       "servers/servers-spec.js",
       "topologies/topologies-spec.js",
       "deliveryServices/delivery-services-spec.js",
-      "jobs/jobs-spec.js"
+      "jobs/jobs-spec.js",
+      "tenants/tenants-spec.js"
     ]
   }
 }
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49f073d..1b7ece9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 
 ## [unreleased]
 ### Added
+- Traffic Portal: [#5394](https://github.com/apache/trafficcontrol/issues/5394) - Converts the tenant table to a tenant tree for usability
 - Traffic Ops: added a feature so that the user can specify `maxRequestHeaderBytes` on a per delivery service basis
 - Traffic Router: log warnings when requests to Traffic Monitor return a 503 status code
 - [#5344](https://github.com/apache/trafficcontrol/issues/5344) - Add a page that addresses migrating from Traffic Ops API v1 for each endpoint
diff --git a/traffic_portal/app/src/common/api/TenantService.js b/traffic_portal/app/src/common/api/TenantService.js
index eaef758..15f194b 100644
--- a/traffic_portal/app/src/common/api/TenantService.js
+++ b/traffic_portal/app/src/common/api/TenantService.js
@@ -72,7 +72,6 @@ var TenantService = function($http, ENV, messageModel) {
     this.deleteTenant = function(id) {
         return $http.delete(ENV.api['root'] + "tenants/" + id).then(
             function(result) {
-                messageModel.setMessages([ { level: 'success', text: 'Tenant deleted' } ], true);
                 return result;
             },
             function(err) {
diff --git a/traffic_portal/app/src/common/modules/form/tenant/FormTenantController.js b/traffic_portal/app/src/common/modules/form/tenant/FormTenantController.js
index fd37399..c7ee43d 100644
--- a/traffic_portal/app/src/common/modules/form/tenant/FormTenantController.js
+++ b/traffic_portal/app/src/common/modules/form/tenant/FormTenantController.js
@@ -45,10 +45,6 @@ var FormTenantController = function(tenant, $scope, $location, formUtils, tenant
         $location.path($location.path() + '/users');
     };
 
-    $scope.viewDSs = function() {
-        $location.path($location.path() + '/delivery-services');
-    };
-
     $scope.navigateToPath = locationUtils.navigateToPath;
 
     $scope.hasError = formUtils.hasError;
diff --git a/traffic_portal/app/src/common/modules/form/tenant/edit/FormEditTenantController.js b/traffic_portal/app/src/common/modules/form/tenant/edit/FormEditTenantController.js
index eea9d18..e2a72bd 100644
--- a/traffic_portal/app/src/common/modules/form/tenant/edit/FormEditTenantController.js
+++ b/traffic_portal/app/src/common/modules/form/tenant/edit/FormEditTenantController.js
@@ -17,14 +17,15 @@
  * under the License.
  */
 
-var FormEditTenantController = function(tenant, $scope, $controller, $uibModal, $anchorScroll, locationUtils, tenantService) {
+var FormEditTenantController = function(tenant, $scope, $controller, $uibModal, $anchorScroll, locationUtils, tenantService, messageModel) {
 
     // extends the FormTenantController to inherit common methods
     angular.extend(this, $controller('FormTenantController', { tenant: tenant, $scope: $scope }));
 
     var deleteTenant = function(tenant) {
         tenantService.deleteTenant(tenant.id)
-            .then(function() {
+            .then(function(result) {
+                messageModel.setMessages(result.data.alerts, true);
                 locationUtils.navigateToPath('/tenants');
             });
     };
@@ -68,5 +69,5 @@ var FormEditTenantController = function(tenant, $scope, $controller, $uibModal,
 
 };
 
-FormEditTenantController.$inject = ['tenant', '$scope', '$controller', '$uibModal', '$anchorScroll', 'locationUtils', 'tenantService'];
+FormEditTenantController.$inject = ['tenant', '$scope', '$controller', '$uibModal', '$anchorScroll', 'locationUtils', 'tenantService', 'messageModel'];
 module.exports = FormEditTenantController;
diff --git a/traffic_portal/app/src/common/modules/form/tenant/form.tenant.tpl.html b/traffic_portal/app/src/common/modules/form/tenant/form.tenant.tpl.html
index 4aac13e..92770a0 100644
--- a/traffic_portal/app/src/common/modules/form/tenant/form.tenant.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/tenant/form.tenant.tpl.html
@@ -24,8 +24,17 @@ under the License.
             <li class="active">{{tenantName}}</li>
         </ol>
         <div class="pull-right" role="group" ng-show="!settings.isNew">
-            <button class="btn btn-primary" title="View Users" ng-click="viewUsers()">View Users</button>
-            <button class="btn btn-primary" title="View Delivery Services" ng-click="viewDSs()">View Delivery Services</button>
+            <button class="btn btn-default" title="View Users" ng-click="viewUsers()">View Users</button>
+            <div class="btn-group" role="group" uib-dropdown is-open="more.isopen">
+                <button name="moreBtn" type="button" class="btn btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+                    View Delivery Services&nbsp;
+                    <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+                    <li role="menuitem"><a href="/#!/tenants/{{tenant.id}}/delivery-services">Assigned To Tenant</a></li>
+                    <li role="menuitem"><a href="/#!/tenants/{{tenant.id}}/delivery-services?all=true">Accessible To Tenant</a></li>
+                </ul>
+            </div>
         </div>
         <div class="clearfix"></div>
     </div>
diff --git a/traffic_portal/app/src/common/modules/form/user/FormUserController.js b/traffic_portal/app/src/common/modules/form/user/FormUserController.js
index a805b07..6d12712 100644
--- a/traffic_portal/app/src/common/modules/form/user/FormUserController.js
+++ b/traffic_portal/app/src/common/modules/form/user/FormUserController.js
@@ -49,10 +49,6 @@ var FormUserController = function(user, $scope, $location, formUtils, stringUtil
 
     $scope.labelize = stringUtils.labelize;
 
-    $scope.viewDeliveryServices = function() {
-        $location.path('/tenants/' + user.tenantId + '/delivery-services');
-    };
-
     $scope.navigateToPath = locationUtils.navigateToPath;
 
     $scope.hasError = formUtils.hasError;
diff --git a/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html b/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
index 875e2a8..a46a0f6 100644
--- a/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/user/form.user.tpl.html
@@ -25,7 +25,7 @@ under the License.
             <li class="active">{{userName}}</li>
         </ol>
         <div class="pull-right" role="group" ng-show="!settings.isNew">
-            <button class="btn btn-primary" title="View Delivery Services" ng-click="viewDeliveryServices()">View Delivery Services</button>
+            <a class="btn btn-primary" ng-href="/#!/tenants/{{user.tenantId}}/delivery-services?all=true">View Delivery Services</a>
         </div>
         <div class="clearfix"></div>
     </div>
diff --git a/traffic_portal/app/src/common/modules/table/tenants/TableTenantsController.js b/traffic_portal/app/src/common/modules/table/tenants/TableTenantsController.js
index 3ddce63..9a2a4af 100644
--- a/traffic_portal/app/src/common/modules/table/tenants/TableTenantsController.js
+++ b/traffic_portal/app/src/common/modules/table/tenants/TableTenantsController.js
@@ -17,37 +17,63 @@
  * under the License.
  */
 
-var TableTenantsController = function(currentUserTenant, tenants, $scope, $state, $timeout, locationUtils, tenantUtils) {
+var TableTenantsController = function(currentUserTenant, tenants, $scope, $state, $timeout, $uibModal, locationUtils, fileUtils, tenantUtils, tenantService, messageModel) {
 
-    $scope.isUserTenant = function(tenant) {
-        return tenant.id == currentUserTenant.id;
-    };
+    $scope.tenantTree = [];
 
-    $scope.editTenant = function(id) {
-        locationUtils.navigateToPath('/tenants/' + id);
+    $scope.hasChildren = function(node) {
+        return node.children.length > 0;
     };
 
-    $scope.createTenant = function() {
-        locationUtils.navigateToPath('/tenants/new');
+    $scope.toggle = function(scope) {
+        scope.toggle();
     };
 
-    var init = function() {
+    $scope.createTenant = function(parentId) {
+        if (parentId) {
+            locationUtils.navigateToPath('/tenants/new?parentId=' + parentId);
+        } else {
+            locationUtils.navigateToPath('/tenants/new');
+        }
+    };
 
-        $scope.tenants = tenantUtils.hierarchySort(tenantUtils.groupTenantsByParent(tenants), currentUserTenant.parentId, []);
-        tenantUtils.addLevels($scope.tenants);
+    $scope.exportCSV = function() {
+        fileUtils.convertToCSV(tenants, 'Tenants', ['id', 'lastUpdated', 'name', 'active', 'parentId', 'parentName']);
+    };
 
-        $timeout(function () {
-            $('#tenantsTable').dataTable({
-                "aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
-                "iDisplayLength": -1,
-                "bSort": false
-            });
-        }, 100);
+    $scope.confirmDelete = function(tenant) {
+        const params = {
+            title: 'Delete Tenant: ' + tenant.name,
+            key: tenant.name
+        };
+        const modalInstance = $uibModal.open({
+            templateUrl: 'common/modules/dialog/delete/dialog.delete.tpl.html',
+            controller: 'DialogDeleteController',
+            size: 'md',
+            resolve: {
+                params: function () {
+                    return params;
+                }
+            }
+        });
+        modalInstance.result.then(function() {
+            tenantService.deleteTenant(tenant.id)
+                .then(function(result) {
+                    messageModel.setMessages(result.data.alerts, false);
+                    $state.reload();
+                });
+        }, function () {
+            // do nothing
+        });
+    };
 
+    let init = function() {
+        $scope.tenants = tenantUtils.hierarchySort(tenantUtils.groupTenantsByParent(tenants), currentUserTenant.parentId, []);
+        $scope.tenantTree = tenantUtils.convertToHierarchy($scope.tenants);
     };
     init();
 
 };
 
-TableTenantsController.$inject = ['currentUserTenant', 'tenants', '$scope', '$state', '$timeout', 'locationUtils', 'tenantUtils'];
+TableTenantsController.$inject = ['currentUserTenant', 'tenants', '$scope', '$state', '$timeout', '$uibModal', 'locationUtils', 'fileUtils', 'tenantUtils', 'tenantService', 'messageModel'];
 module.exports = TableTenantsController;
diff --git a/traffic_portal/app/src/common/modules/table/tenants/table.tenants.tpl.html b/traffic_portal/app/src/common/modules/table/tenants/table.tenants.tpl.html
index b93b825..ea95cf7 100644
--- a/traffic_portal/app/src/common/modules/table/tenants/table.tenants.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/tenants/table.tenants.tpl.html
@@ -20,35 +20,58 @@ under the License.
 <div class="x_panel">
     <div class="x_title">
         <ol class="breadcrumb pull-left">
-            <li class="active">Tenants</li>
+            <li class="active">Tenants <small>[{{tenants.length}}]</small></li>
         </ol>
         <div class="pull-right">
-            <button class="btn btn-primary" title="Create New Tenant" ng-click="createTenant()"><i class="fa fa-plus"></i></button>
-            <button class="btn btn-default" title="Refresh" ng-click="refresh()"><i class="fa fa-refresh"></i></button>
+            <button name="createTenantButton" class="btn btn-primary" title="Create New Tenant" ng-click="createTenant()"><i class="fa fa-plus"></i></button>
+            <div class="btn-group" role="group" uib-dropdown is-open="more.isopen">
+                <button name="moreBtn" type="button" class="btn btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+                    More&nbsp;
+                    <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+                    <li role="menuitem"><a name="createServerMenuItem" ng-click="exportCSV()">Export Tenant CSV</a></li>
+                </ul>
+            </div>
         </div>
         <div class="clearfix"></div>
     </div>
     <div class="x_content">
         <br>
-        <table id="tenantsTable" class="table responsive-utilities jambo_table">
-            <thead>
-            <tr class="headings">
-                <th>Name</th>
-                <th>Active</th>
-                <th>Parent</th>
-            </tr>
-            </thead>
-            <tbody>
-            <tr ng-click="editTenant(t.id)" ng-repeat="t in ::tenants" ng-class="::{'active': isUserTenant(t)}">
-                <td name="name" data-search="^{{::t.name}}$">{{::t.name}}</td>
-                <td data-search="^{{::t.active}}$">{{::t.active}}</td>
-                <td data-search="^{{::t.parentName}}$">{{::t.parentName}}</td>
-            </tr>
-            </tbody>
-        </table>
+        <div id="tenant-tree-root" ui-tree data-drag-enabled="false">
+            <ol ui-tree-nodes ng-model="tenantTree">
+                <li ng-repeat="node in tenantTree" ui-tree-node ng-include="'nodes_renderer.html'"></li>
+            </ol>
+        </div>
     </div>
 </div>
 
+<script type="text/ng-template" id="nodes_renderer.html">
+    <div ui-tree-handle class="tree-node tree-node-content">
+        <div class="tree-node-label pull-left">
+            <a class="tree-toggle btn btn-primary btn-xs" ng-if="hasChildren(node)" ng-click="toggle(this)">
+                <i class="fa" ng-class="collapsed ? 'fa-caret-right' : 'fa-caret-down'"></i>
+            </a> <a ng-href="#!/tenants/{{node.id}}">{{::node.name}}</a>
+        </div>
+        <div class="pull-right">
+            <a title="Add child tenant" class="btn btn-primary btn-xs" data-nodrag ng-click="createTenant(node.id)" style="margin-right: 8px;">
+                <i class="fa fa-plus"></i>
+            </a>
+            <a title="Delete tenant" class="btn btn-danger btn-xs" data-nodrag ng-click="confirmDelete(node)" style="margin-right: 8px;">
+                <i class="fa fa-times"></i>
+            </a>
+            <a title="Edit tenant" class="btn btn-default btn-xs" data-nodrag ng-href="#!/tenants/{{node.id}}">
+                <i class="fa fa-edit"></i>
+            </a>
+        </div>
+    </div>
+    <ol ui-tree-nodes="" ng-model="node.children" ng-class="{ hidden: collapsed }">
+        <li ng-repeat="node in node.children" ui-tree-node ng-include="'nodes_renderer.html'"></li>
+    </ol>
+</script>
+
+
+
 
 
 
diff --git a/traffic_portal/app/src/common/service/utils/FileUtils.js b/traffic_portal/app/src/common/service/utils/FileUtils.js
index f01cf8a..53482c0 100644
--- a/traffic_portal/app/src/common/service/utils/FileUtils.js
+++ b/traffic_portal/app/src/common/service/utils/FileUtils.js
@@ -31,6 +31,56 @@ var FileUtils = function() {
 		a.remove();
 	};
 
+	this.convertToCSV = function(JSONData, reportTitle, includedKeys) {
+		var arrData = typeof JSONData != 'object' ? JSON.parse(JSONData) : JSONData;
+		var CSV = '';
+		CSV += reportTitle + '\r\n\r\n';
+
+		var keys = [];
+		for (var key in arrData[0]) {
+			if (!includedKeys || _.contains(includedKeys, key)) {
+				keys.push(key);
+			}
+		}
+		keys.sort(); // alphabetically
+
+		var row = "";
+		for (var i = 0; i < keys.length; i++) {
+			row += keys[i] + ',';
+		}
+		row = row.slice(0, -1);
+
+		CSV += row + '\r\n';
+
+		for (var j = 0; j < arrData.length; j++) {
+			var row = "";
+			for (var k = 0; k < keys.length; k++) {
+				row += '"' + arrData[j][keys[k]] + '",';
+			}
+			row.slice(0, row.length - 1);
+			CSV += row + '\r\n';
+		}
+
+		if (CSV == '') {
+			alert("Invalid data");
+			return;
+		}
+
+		var fileName = "";
+		fileName += reportTitle.replace(/ /g,"_");
+
+		var uri = 'data:text/csv;charset=utf-8,' + escape(CSV);
+		var link = document.createElement("a");
+		link.href = uri;
+
+		link.style = "visibility:hidden";
+		link.download = fileName + ".csv";
+
+		document.body.appendChild(link);
+		link.click();
+		document.body.removeChild(link);
+	};
+
 };
 
 FileUtils.$inject = [];
diff --git a/traffic_portal/app/src/modules/private/tenants/deliveryServices/index.js b/traffic_portal/app/src/modules/private/tenants/deliveryServices/index.js
index f17edf2..b633ac4 100644
--- a/traffic_portal/app/src/modules/private/tenants/deliveryServices/index.js
+++ b/traffic_portal/app/src/modules/private/tenants/deliveryServices/index.js
@@ -21,7 +21,7 @@ module.exports = angular.module('trafficPortal.private.tenants.deliveryServices'
 	.config(function($stateProvider, $urlRouterProvider) {
 		$stateProvider
 			.state('trafficPortal.private.tenants.deliveryServices', {
-				url: '/{tenantId}/delivery-services',
+				url: '/{tenantId}/delivery-services?all',
 				views: {
 					tenantsContent: {
 						templateUrl: 'common/modules/table/tenantDeliveryServices/table.tenantDeliveryServices.tpl.html',
@@ -30,8 +30,11 @@ module.exports = angular.module('trafficPortal.private.tenants.deliveryServices'
 							tenant: function($stateParams, tenantService) {
 								return tenantService.getTenant($stateParams.tenantId);
 							},
-							deliveryServices: function(tenant, deliveryServiceService) {
-								return deliveryServiceService.getDeliveryServices({ accessibleTo: tenant.id });
+							deliveryServices: function($stateParams, tenant, deliveryServiceService) {
+								if ($stateParams.all && $stateParams.all === 'true') {
+									return deliveryServiceService.getDeliveryServices({ accessibleTo: tenant.id });
+								}
+								return deliveryServiceService.getDeliveryServices({ tenant: tenant.id });
 							}
 						}
 					}
diff --git a/traffic_portal/app/src/modules/private/tenants/new/index.js b/traffic_portal/app/src/modules/private/tenants/new/index.js
index 2b8e516..ad168bb 100644
--- a/traffic_portal/app/src/modules/private/tenants/new/index.js
+++ b/traffic_portal/app/src/modules/private/tenants/new/index.js
@@ -21,13 +21,16 @@ module.exports = angular.module('trafficPortal.private.tenants.new', [])
     .config(function($stateProvider, $urlRouterProvider) {
         $stateProvider
             .state('trafficPortal.private.tenants.new', {
-                url: '/new',
+                url: '/new?parentId',
                 views: {
                     tenantsContent: {
                         templateUrl: 'common/modules/form/tenant/form.tenant.tpl.html',
                         controller: 'FormNewTenantController',
                         resolve: {
-                            tenant: function() {
+                            tenant: function($stateParams) {
+                                if ($stateParams.parentId) {
+                                    return { parentId: parseInt($stateParams.parentId, 10) };
+                                }
                                 return {};
                             }
                         }
diff --git a/traffic_portal/app/src/styles/main.scss b/traffic_portal/app/src/styles/main.scss
index e232b5c..9949ee5 100644
--- a/traffic_portal/app/src/styles/main.scss
+++ b/traffic_portal/app/src/styles/main.scss
@@ -256,3 +256,9 @@ input[type="checkbox"].dirty {
   }
 
 }
+
+#tenant-tree-root {
+  .angular-ui-tree-handle {
+    cursor: default !important;
+  }
+}
diff --git a/traffic_portal/test/end_to_end/conf.json b/traffic_portal/test/end_to_end/conf.json
index 27e1ef0..64f39a1 100644
--- a/traffic_portal/test/end_to_end/conf.json
+++ b/traffic_portal/test/end_to_end/conf.json
@@ -27,7 +27,8 @@
       "servers/servers-spec.js",
       "topologies/topologies-spec.js",
       "deliveryServices/delivery-services-spec.js",
-      "jobs/jobs-spec.js"
+      "jobs/jobs-spec.js",
+      "tenants/tenants-spec.js"
     ]
   }
 }
diff --git a/traffic_portal/app/src/common/service/utils/FileUtils.js b/traffic_portal/test/end_to_end/tenants/pageData.js
similarity index 60%
copy from traffic_portal/app/src/common/service/utils/FileUtils.js
copy to traffic_portal/test/end_to_end/tenants/pageData.js
index f01cf8a..fa2c03c 100644
--- a/traffic_portal/app/src/common/service/utils/FileUtils.js
+++ b/traffic_portal/test/end_to_end/tenants/pageData.js
@@ -17,21 +17,9 @@
  * under the License.
  */
 
-var FileUtils = function() {
-
-	this.exportJSON = function(json, fileName, fileExtension) {
-		var jsonStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(json, null, '\t')), // tab indented
-			extension = fileExtension || 'json';
-
-		// look ma, no hands...anchor trickery to pop a download dialog
-		var a = document.createElement('a');
-		a.setAttribute("href", jsonStr);
-		a.setAttribute("download", fileName + "." + extension);
-		a.click();
-		a.remove();
-	};
-
+module.exports = function(){
+	this.name=element(by.name('name'));
+	this.active=element(by.name('active'));
+	this.parent=element(by.name('parentId'));
+	this.createButton=element(by.buttonText('Create'));
 };
-
-FileUtils.$inject = [];
-module.exports = FileUtils;
diff --git a/traffic_portal/test/end_to_end/tenants/tenants-spec.js b/traffic_portal/test/end_to_end/tenants/tenants-spec.js
new file mode 100644
index 0000000..e0b9ea3
--- /dev/null
+++ b/traffic_portal/test/end_to_end/tenants/tenants-spec.js
@@ -0,0 +1,55 @@
+/*
+ * 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 pd = require('./pageData.js');
+var cfunc = require('../common/commonFunctions.js');
+
+describe('Traffic Portal Tenants Test Suite', function() {
+	const pageData = new pd();
+	const commonFunctions = new cfunc();
+	const myNewTenant = {
+		name: 'tenant-' + commonFunctions.shuffle('abcdefghijklmonpqrstuvwxyz0123456789')
+	};
+
+	it('should go to the tenants page', function() {
+		console.log("Go to the tenants page");
+		browser.setLocation("tenants");
+		expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/tenants");
+	});
+
+	it('should open new tenant form page', function() {
+		console.log("Open new tenant form page");
+		browser.driver.findElement(by.name('createTenantButton')).click();
+		expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/tenants/new");
+	});
+
+	it('should fill out form, create button is enabled and submit', function () {
+		console.log("Filling out form, check create button is enabled and submit");
+		expect(pageData.createButton.isEnabled()).toBe(false);
+		pageData.name.sendKeys(myNewTenant.name);
+		commonFunctions.selectDropdownbyNum(pageData.active, 1);
+		commonFunctions.selectDropdownbyNum(pageData.parent, 1);
+		expect(pageData.createButton.isEnabled()).toBe(true);
+		pageData.createButton.click();
+		expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/tenants");
+	});
+
+
+
+});