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

mitchell852 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 e5d7dc8  Servers table switch to ag-grid (#4769)
e5d7dc8 is described below

commit e5d7dc8a0a63e7fd9917e2fd958dd8c9e7975306
Author: ocket8888 <[email protected]>
AuthorDate: Tue Jun 16 19:41:40 2020 -0600

    Servers table switch to ag-grid (#4769)
    
    * Added ag-grid tables to TP
    
    * Fixed build issues
    
    * Replaced servers table with agGrid
    
    * Switch to tabs
    
    * Started setting up context menu support
    
    * Added some context menu functionality, fixed some styling
    
    * Put styling in the stylesheet; added server delete to context menu
    
    * Moved menu outside of panel
    
    * Added server status update to context menu
    
    * Added queue updates to context menu
    
    * Added clear updates to context menu; added disabled menuitem styling
    
    * Fixed a type error, finished context menu
    
    * Removed unused things, general clean-up and re-organization
    
    * added CSV export
    
    * Rolled back changes to source of AngularJS
    
    * Fixed menuitem button styling
    
    * table now saves sort, filter, and column state
    
    * table now saves column sizes
    
    * Rolled back inneffectual browserify changes
    
    * fixes server TP tests to work with ag-grid
    
    * fixes broken TP ds test
    
    * adds a new super controller for the *servers tables
    
    * Fixed blank CDNs column
    
    * Fixed incorrect update pending label icon
    
    * Removed unused coldef properties, removed unused gridOptions property
    
    * Fixed broken 'show charts' button in servers table context menu
    
    * Fixed context menu not closing when certain actions were selected
    
    Co-authored-by: Jeremy Mitchell <[email protected]>
---
 traffic_portal/app/src/app.js                      |   6 +-
 .../app/src/common/modules/table/_table.scss       |  71 ++
 .../TableCacheGroupServersController.js            |   4 +-
 .../table/cdnServers/TableCDNServersController.js  |   4 +-
 .../TableDeliveryServiceServersController.js       |   4 +-
 .../TablePhysLocationServersController.js          |   4 +-
 .../TableProfileServersController.js               |   4 +-
 .../TableServerCapabilityServersController.js      |   4 +-
 .../table/servers/TableParentServersController.js  | 421 +++++++++
 .../table/servers/TableServersController.js        | 991 ++++++++++++---------
 .../app/src/common/modules/table/servers/index.js  |   3 +-
 .../modules/table/servers/table.servers.tpl.html   | 134 +--
 .../statusServers/TableStatusServersController.js  |   4 +-
 .../typeServers/TableTypeServersController.js      |   4 +-
 traffic_portal/app/src/index.html                  |   1 +
 traffic_portal/grunt/copy.js                       |  18 +-
 traffic_portal/grunt/globalConfig.js               |   5 +-
 traffic_portal/package-lock.json                   |   5 +
 traffic_portal/package.json                        |   3 +
 .../deliveryServices/delivery-services-spec.js     |   2 +-
 .../test/end_to_end/servers/servers-spec.js        |  17 +-
 21 files changed, 1183 insertions(+), 526 deletions(-)

diff --git a/traffic_portal/app/src/app.js b/traffic_portal/app/src/app.js
index ddf01c5..7575d3c 100644
--- a/traffic_portal/app/src/app.js
+++ b/traffic_portal/app/src/app.js
@@ -24,8 +24,11 @@ var App = function($urlRouterProvider) {
     $urlRouterProvider.otherwise('/');
 };
 
+
 App.$inject = ['$urlRouterProvider'];
 
+agGrid.initialiseAgGridWithAngular1(angular);
+
 var trafficPortal = angular.module('trafficPortal', [
         'config',
         'ngAnimate',
@@ -42,6 +45,7 @@ var trafficPortal = angular.module('trafficPortal', [
         'angular-loading-bar',
         'moment-picker',
         'jsonFormatter',
+        'agGrid',
 
         // public modules
         require('./modules/public').name,
@@ -509,5 +513,3 @@ trafficPortal.factory('authInterceptor', function 
($rootScope, $q, $window, $loc
 trafficPortal.config(function ($httpProvider) {
     $httpProvider.interceptors.push('authInterceptor');
 });
-
-
diff --git a/traffic_portal/app/src/common/modules/table/_table.scss 
b/traffic_portal/app/src/common/modules/table/_table.scss
index 1c39b48..e6a2868 100644
--- a/traffic_portal/app/src/common/modules/table/_table.scss
+++ b/traffic_portal/app/src/common/modules/table/_table.scss
@@ -107,3 +107,74 @@ th.center, td.center {
 .dt-button.btn-link {
     text-decoration: underline;
 }
+
+/* Table context menus */
+menu[type="contextmenu"] {
+    display: block;
+    position: fixed;
+    background-color: white;
+    padding: 0;
+    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+    border-radius: 3px;
+
+    ul {
+        display: block;
+        clear: both;
+        font-weight: normal;
+        line-height: 1.428571429;
+        white-space: nowrap;
+        padding: 0;
+        margin: 0;
+
+        li {
+            display: block;
+            clear: both;
+
+            &:hover {
+                color: #262626;
+                background-color: #f5f5f5;
+            }
+
+            a, button {
+                color: #333333;
+                text-decoration: none;
+                padding: 7px 20px;
+                display: block;
+                clear: both;
+            }
+
+            button {
+                background: transparent;
+                border: none;
+                width: 100%;
+                text-align: left;
+
+                &[disabled] {
+                    color: gray;
+                    background-color: #f5f5f5;
+                }
+            }
+        }
+
+        hr.divider {
+            margin: 0;
+        }
+    }
+}
+
+div.dropdown button.menu-item-button {
+    color: #333333;
+    width: 100%;
+    background: transparent;
+    border: none;
+    text-align: inherit;
+    display: block;
+    clear: both;
+    padding: 3px 20px;
+    margin: 0;
+
+    &:hover {
+        color: #262626;
+        background-color: #f5f5f5;
+    }
+}
diff --git 
a/traffic_portal/app/src/common/modules/table/cacheGroupServers/TableCacheGroupServersController.js
 
b/traffic_portal/app/src/common/modules/table/cacheGroupServers/TableCacheGroupServersController.js
index 7ce6742..8ea50a2 100644
--- 
a/traffic_portal/app/src/common/modules/table/cacheGroupServers/TableCacheGroupServersController.js
+++ 
b/traffic_portal/app/src/common/modules/table/cacheGroupServers/TableCacheGroupServersController.js
@@ -19,8 +19,8 @@
 
 var TableCacheGroupsServersController = function(cacheGroup, servers, 
$controller, $scope, $state, $uibModal, cacheGroupService) {
 
-       // extends the TableServersController to inherit common methods
-       angular.extend(this, $controller('TableServersController', { servers: 
servers, $scope: $scope }));
+       // extends the TableParentServersController to inherit common methods
+       angular.extend(this, $controller('TableParentServersController', { 
servers: servers, $scope: $scope }));
 
        let cacheGroupServersTable;
 
diff --git 
a/traffic_portal/app/src/common/modules/table/cdnServers/TableCDNServersController.js
 
b/traffic_portal/app/src/common/modules/table/cdnServers/TableCDNServersController.js
index f0b8df7..b6a4dca 100644
--- 
a/traffic_portal/app/src/common/modules/table/cdnServers/TableCDNServersController.js
+++ 
b/traffic_portal/app/src/common/modules/table/cdnServers/TableCDNServersController.js
@@ -19,8 +19,8 @@
 
 var TableCDNServersController = function(cdn, servers, $controller, $scope) {
 
-       // extends the TableServersController to inherit common methods
-       angular.extend(this, $controller('TableServersController', { servers: 
servers, $scope: $scope }));
+       // extends the TableParentServersController to inherit common methods
+       angular.extend(this, $controller('TableParentServersController', { 
servers: servers, $scope: $scope }));
 
        let cdnServersTable;
 
diff --git 
a/traffic_portal/app/src/common/modules/table/deliveryServiceServers/TableDeliveryServiceServersController.js
 
b/traffic_portal/app/src/common/modules/table/deliveryServiceServers/TableDeliveryServiceServersController.js
index b343657..5d0c094 100644
--- 
a/traffic_portal/app/src/common/modules/table/deliveryServiceServers/TableDeliveryServiceServersController.js
+++ 
b/traffic_portal/app/src/common/modules/table/deliveryServiceServers/TableDeliveryServiceServersController.js
@@ -19,8 +19,8 @@
 
 var TableDeliveryServiceServersController = function(deliveryService, servers, 
$controller, $scope, $uibModal, deliveryServiceService, serverUtils) {
 
-       // extends the TableServersController to inherit common methods
-       angular.extend(this, $controller('TableServersController', { servers: 
servers, $scope: $scope }));
+       // extends the TableParentServersController to inherit common methods
+       angular.extend(this, $controller('TableParentServersController', { 
servers: servers, $scope: $scope }));
 
        let dsServersTable;
 
diff --git 
a/traffic_portal/app/src/common/modules/table/physLocationServers/TablePhysLocationServersController.js
 
b/traffic_portal/app/src/common/modules/table/physLocationServers/TablePhysLocationServersController.js
index 497eb6c..f329796 100644
--- 
a/traffic_portal/app/src/common/modules/table/physLocationServers/TablePhysLocationServersController.js
+++ 
b/traffic_portal/app/src/common/modules/table/physLocationServers/TablePhysLocationServersController.js
@@ -19,8 +19,8 @@
 
 var TablePhysLocationServersController = function(physLocation, servers, 
$controller, $scope) {
 
-       // extends the TableServersController to inherit common methods
-       angular.extend(this, $controller('TableServersController', { servers: 
servers, $scope: $scope }));
+       // extends the TableParentServersController to inherit common methods
+       angular.extend(this, $controller('TableParentServersController', { 
servers: servers, $scope: $scope }));
 
        let physLocServersTable;
 
diff --git 
a/traffic_portal/app/src/common/modules/table/profileServers/TableProfileServersController.js
 
b/traffic_portal/app/src/common/modules/table/profileServers/TableProfileServersController.js
index dbc2cf7..eff77f2 100644
--- 
a/traffic_portal/app/src/common/modules/table/profileServers/TableProfileServersController.js
+++ 
b/traffic_portal/app/src/common/modules/table/profileServers/TableProfileServersController.js
@@ -19,8 +19,8 @@
 
 var TableProfileServersController = function(profile, servers, $controller, 
$scope) {
 
-       // extends the TableServersController to inherit common methods
-       angular.extend(this, $controller('TableServersController', { servers: 
servers, $scope: $scope }));
+       // extends the TableParentServersController to inherit common methods
+       angular.extend(this, $controller('TableParentServersController', { 
servers: servers, $scope: $scope }));
 
        let profileServersTable;
 
diff --git 
a/traffic_portal/app/src/common/modules/table/serverCapabilityServers/TableServerCapabilityServersController.js
 
b/traffic_portal/app/src/common/modules/table/serverCapabilityServers/TableServerCapabilityServersController.js
index a6dad82..1afca89 100644
--- 
a/traffic_portal/app/src/common/modules/table/serverCapabilityServers/TableServerCapabilityServersController.js
+++ 
b/traffic_portal/app/src/common/modules/table/serverCapabilityServers/TableServerCapabilityServersController.js
@@ -19,8 +19,8 @@
 
 var TableServerCapabilityServersController = function(serverCapability, 
servers, $scope, $state, $controller, $uibModal, $window, locationUtils, 
serverService, messageModel) {
 
-       // extends the TableServersController to inherit common methods
-       angular.extend(this, $controller('TableServersController', { servers: 
servers, $scope: $scope }));
+       // extends the TableParentServersController to inherit common methods
+       angular.extend(this, $controller('TableParentServersController', { 
servers: servers, $scope: $scope }));
 
        var removeCapability = function(serverId) {
                serverService.removeServerCapability(serverId, 
serverCapability.name)
diff --git 
a/traffic_portal/app/src/common/modules/table/servers/TableParentServersController.js
 
b/traffic_portal/app/src/common/modules/table/servers/TableParentServersController.js
new file mode 100644
index 0000000..5fb0adc
--- /dev/null
+++ 
b/traffic_portal/app/src/common/modules/table/servers/TableParentServersController.js
@@ -0,0 +1,421 @@
+/*
+ * 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 TableParentServersController = function(servers, $scope, $state, 
$uibModal, $window, dateUtils, locationUtils, serverUtils, cdnService, 
serverService, statusService, propertiesModel, messageModel) {
+
+       let serversTable;
+
+       var getStatuses = function() {
+               statusService.getStatuses()
+                       .then(function(result) {
+                               $scope.statuses = result;
+                       });
+       };
+
+       var queueServerUpdates = function(server) {
+               serverService.queueServerUpdates(server.id)
+                       .then(
+                               function() {
+                                       $scope.refresh();
+                               }
+                       );
+       };
+
+       var clearServerUpdates = function(server) {
+               serverService.clearServerUpdates(server.id)
+                       .then(
+                               function() {
+                                       $scope.refresh();
+                               }
+                       );
+       };
+
+       var queueCDNServerUpdates = function(cdnId) {
+               cdnService.queueServerUpdates(cdnId)
+                       .then(
+                               function() {
+                                       $scope.refresh();
+                               }
+                       );
+       };
+
+       var clearCDNServerUpdates = function(cdnId) {
+               cdnService.clearServerUpdates(cdnId)
+                       .then(
+                               function() {
+                                       $scope.refresh();
+                               }
+                       );
+       };
+
+       var confirmDelete = function(server) {
+               var params = {
+                       title: 'Delete Server: ' + server.hostName,
+                       key: server.hostName
+               };
+               var 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() {
+                       deleteServer(server);
+               }, function () {
+                       // do nothing
+               });
+       };
+
+       var deleteServer = function(server) {
+               serverService.deleteServer(server.id)
+                       .then(function(result) {
+                               messageModel.setMessages(result.alerts, false);
+                               $scope.refresh();
+                       });
+       };
+
+       var confirmStatusUpdate = function(server) {
+               var modalInstance = $uibModal.open({
+                       templateUrl: 
'common/modules/dialog/select/status/dialog.select.status.tpl.html',
+                       controller: 'DialogSelectStatusController',
+                       size: 'md',
+                       resolve: {
+                               server: function() {
+                                       return server;
+                               },
+                               statuses: function() {
+                                       return $scope.statuses;
+                               }
+                       }
+               });
+               modalInstance.result.then(function(status) {
+                       updateStatus(status, server);
+               }, function () {
+                       // do nothing
+               });
+       };
+
+       var updateStatus = function(status, server) {
+               serverService.updateStatus(server.id, { status: status.id, 
offlineReason: status.offlineReason })
+                       .then(
+                               function(result) {
+                                       
messageModel.setMessages(result.data.alerts, false);
+                                       $scope.refresh();
+                               },
+                               function(fault) {
+                                       
messageModel.setMessages(fault.data.alerts, false);
+                               }
+                       );
+       };
+
+       $scope.servers = servers;
+
+       $scope.columns = [
+               { "name": "Cache Group", "visible": true, "searchable": true },
+               { "name": "CDN", "visible": true, "searchable": true },
+               { "name": "Domain", "visible": true, "searchable": true },
+               { "name": "Host", "visible": true, "searchable": true },
+               { "name": "HTTPS Port", "visible": false, "searchable": false },
+               { "name": "ID", "visible": false, "searchable": false },
+               { "name": "ILO IP Address", "visible": true, "searchable": true 
},
+               { "name": "ILO IP Gateway", "visible": false, "searchable": 
false },
+               { "name": "ILO IP Netmask", "visible": false, "searchable": 
false },
+               { "name": "ILO Username", "visible": false, "searchable": false 
},
+               { "name": "Interface Name", "visible": false, "searchable": 
false },
+               { "name": "IPv6 Address", "visible": true, "searchable": true },
+               { "name": "IPv6 Gateway", "visible": false, "searchable": false 
},
+               { "name": "Last Updated", "visible": false, "searchable": false 
},
+               { "name": "Mgmt IP Address", "visible": false, "searchable": 
false },
+               { "name": "Mgmt IP Gateway", "visible": false, "searchable": 
false },
+               { "name": "Mgmt IP Netmask", "visible": false, "searchable": 
false },
+               { "name": "Network Gateway", "visible": false, "searchable": 
false },
+               { "name": "Network IP", "visible": true, "searchable": true },
+               { "name": "Network MTU", "visible": false, "searchable": false 
},
+               { "name": "Network Subnet", "visible": false, "searchable": 
false },
+               { "name": "Offline Reason", "visible": false, "searchable": 
false },
+               { "name": "Phys Location", "visible": true, "searchable": true 
},
+               { "name": "Profile", "visible": true, "searchable": true },
+               { "name": "Rack", "visible": false, "searchable": false },
+               { "name": "Reval Pending", "visible": false, "searchable": 
false },
+               { "name": "Router Hostname", "visible": false, "searchable": 
false },
+               { "name": "Router Port Name", "visible": false, "searchable": 
false },
+               { "name": "Status", "visible": true, "searchable": true },
+               { "name": "TCP Port", "visible": false, "searchable": false },
+               { "name": "Type", "visible": true, "searchable": true },
+               { "name": "Update Pending", "visible": true, "searchable": true 
}
+       ];
+
+       $scope.contextMenuItems = [
+               {
+                       text: 'Open in New Tab',
+                       click: function ($itemScope) {
+                               $window.open('/#!/servers/' + $itemScope.s.id, 
'_blank');
+                       }
+               },
+               null, // Divider
+               {
+                       text: 'Navigate to Server FQDN',
+                       click: function ($itemScope) {
+                               $window.open('http://' + $itemScope.s.hostName 
+ '.' + $itemScope.s.domainName, '_blank');
+                       }
+               },
+               null, // Divider
+               {
+                       text: 'Edit',
+                       click: function ($itemScope) {
+                               $scope.editServer($itemScope.s.id);
+                       }
+               },
+               {
+                       text: 'Delete',
+                       click: function ($itemScope) {
+                               confirmDelete($itemScope.s);
+                       }
+               },
+               null, // Divider
+               {
+                       text: 'Update Status',
+                       click: function ($itemScope) {
+                               confirmStatusUpdate($itemScope.s);
+                       }
+               },
+               {
+                       text: 'Queue Server Updates',
+                       displayed: function ($itemScope) {
+                               return serverUtils.isCache($itemScope.s) && 
!$itemScope.s.updPending;
+                       },
+                       click: function ($itemScope) {
+                               queueServerUpdates($itemScope.s);
+                       }
+               },
+               {
+                       text: 'Clear Server Updates',
+                       displayed: function ($itemScope) {
+                               return serverUtils.isCache($itemScope.s) && 
$itemScope.s.updPending;
+                       },
+                       click: function ($itemScope) {
+                               clearServerUpdates($itemScope.s);
+                       }
+               },
+               {
+                       text: 'Show Charts',
+                       displayed: function () {
+                               return 
propertiesModel.properties.servers.charts.show;
+                       },
+                       hasBottomDivider: function () {
+                               return true;
+                       },
+                       hasTopDivider: function () {
+                               return true;
+                       },
+                       click: function ($itemScope) {
+                               
$window.open(propertiesModel.properties.servers.charts.baseUrl + 
$itemScope.s.hostName, '_blank');
+                       }
+               },
+               {
+                       text: 'Manage Capabilities',
+                       displayed: function ($itemScope) {
+                               return serverUtils.isCache($itemScope.s);
+                       },
+                       hasTopDivider: function () {
+                               return true;
+                       },
+                       click: function ($itemScope) {
+                               locationUtils.navigateToPath('/servers/' + 
$itemScope.s.id + '/capabilities');
+                       }
+               },
+               {
+                       text: 'Manage Delivery Services',
+                       displayed: function ($itemScope) {
+                               return serverUtils.isEdge($itemScope.s) || 
serverUtils.isOrigin($itemScope.s);
+                       },
+                       hasTopDivider: function ($itemScope) {
+                               return !serverUtils.isCache($itemScope.s);
+                       },
+                       click: function ($itemScope) {
+                               locationUtils.navigateToPath('/servers/' + 
$itemScope.s.id + '/delivery-services');
+                       }
+               },
+               {
+                       text: 'View Config Files',
+                       displayed: function ($itemScope) {
+                               return serverUtils.isCache($itemScope.s);
+                       },
+                       click: function ($itemScope) {
+                               locationUtils.navigateToPath('/servers/' + 
$itemScope.s.id + '/config-files');
+                       }
+               }
+       ];
+
+       $scope.editServer = function(id) {
+               locationUtils.navigateToPath('/servers/' + id);
+       };
+
+       $scope.createServer = function() {
+               locationUtils.navigateToPath('/servers/new');
+       };
+
+       $scope.confirmCDNQueueServerUpdates = function(cdn) {
+               var params;
+               if (cdn) {
+                       params = {
+                               title: 'Queue Server Updates: ' + cdn.name,
+                               message: 'Are you sure you want to queue server 
updates for all ' + cdn.name + ' servers?'
+                       };
+                       var modalInstance = $uibModal.open({
+                               templateUrl: 
'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+                               controller: 'DialogConfirmController',
+                               size: 'md',
+                               resolve: {
+                                       params: function () {
+                                               return params;
+                                       }
+                               }
+                       });
+                       modalInstance.result.then(function() {
+                               queueCDNServerUpdates(cdn.id);
+                       }, function () {
+                               // do nothing
+                       });
+               } else {
+                       params = {
+                               title: 'Queue Server Updates',
+                               message: "Please select a CDN"
+                       };
+                       var modalInstance = $uibModal.open({
+                               templateUrl: 
'common/modules/dialog/select/dialog.select.tpl.html',
+                               controller: 'DialogSelectController',
+                               size: 'md',
+                               resolve: {
+                                       params: function () {
+                                               return params;
+                                       },
+                                       collection: function(cdnService) {
+                                               return cdnService.getCDNs();
+                                       }
+                               }
+                       });
+                       modalInstance.result.then(function(cdn) {
+                               queueCDNServerUpdates(cdn.id);
+                       }, function () {
+                               // do nothing
+                       });
+               }
+       };
+
+       $scope.confirmCDNClearServerUpdates = function(cdn) {
+               var params;
+               if (cdn) {
+                       params = {
+                               title: 'Clear Server Updates: ' + cdn.name,
+                               message: 'Are you sure you want to clear server 
updates for all ' + cdn.name + ' servers?'
+                       };
+                       var modalInstance = $uibModal.open({
+                               templateUrl: 
'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+                               controller: 'DialogConfirmController',
+                               size: 'md',
+                               resolve: {
+                                       params: function () {
+                                               return params;
+                                       }
+                               }
+                       });
+                       modalInstance.result.then(function() {
+                               clearCDNServerUpdates(cdn.id);
+                       }, function () {
+                               // do nothing
+                       });
+
+
+               } else {
+                       params = {
+                               title: 'Clear Server Updates',
+                               message: "Please select a CDN"
+                       };
+                       var modalInstance = $uibModal.open({
+                               templateUrl: 
'common/modules/dialog/select/dialog.select.tpl.html',
+                               controller: 'DialogSelectController',
+                               size: 'md',
+                               resolve: {
+                                       params: function () {
+                                               return params;
+                                       },
+                                       collection: function(cdnService) {
+                                               return cdnService.getCDNs();
+                                       }
+                               }
+                       });
+                       modalInstance.result.then(function(cdn) {
+                               clearCDNServerUpdates(cdn.id);
+                       }, function () {
+                               // do nothing
+                       });
+               }
+       };
+
+       $scope.refresh = function() {
+               $state.reload(); // reloads all the resolves for the view
+       };
+
+       $scope.toggleVisibility = function(colName) {
+               const col = serversTable.column(colName + ':name');
+               col.visible(!col.visible());
+               serversTable.rows().invalidate().draw();
+       };
+
+       $scope.ssh = serverUtils.ssh;
+
+       $scope.isOffline = serverUtils.isOffline;
+
+       $scope.offlineReason = serverUtils.offlineReason;
+
+       $scope.getRelativeTime = dateUtils.getRelativeTime;
+
+       $scope.navigateToPath = locationUtils.navigateToPath;
+
+       var init = function () {
+               getStatuses();
+       };
+       init();
+
+       angular.element(document).ready(function () {
+               serversTable = $('#serversTable').DataTable({
+                       "lengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
+                       "iDisplayLength": 25,
+                       "aaSorting": [],
+                       "columns": $scope.columns,
+                       "initComplete": function(settings, json) {
+                               try {
+                                       // need to create the show/hide column 
checkboxes and bind to the current visibility
+                                       $scope.columns = 
JSON.parse(localStorage.getItem('DataTables_serversTable_/')).columns;
+                               } catch (e) {
+                                       console.error("Failure to retrieve 
required column info from localStorage (key=DataTables_serversTable_/):", e);
+                               }
+                       }
+               });
+       });
+
+};
+
+TableParentServersController.$inject = ['servers', '$scope', '$state', 
'$uibModal', '$window', 'dateUtils', 'locationUtils', 'serverUtils', 
'cdnService', 'serverService', 'statusService', 'propertiesModel', 
'messageModel'];
+module.exports = TableParentServersController;
diff --git 
a/traffic_portal/app/src/common/modules/table/servers/TableServersController.js 
b/traffic_portal/app/src/common/modules/table/servers/TableServersController.js
index 4073abe..10ad816 100644
--- 
a/traffic_portal/app/src/common/modules/table/servers/TableServersController.js
+++ 
b/traffic_portal/app/src/common/modules/table/servers/TableServersController.js
@@ -17,405 +17,600 @@
  * under the License.
  */
 
-var TableServersController = function(servers, $scope, $state, $uibModal, 
$window, dateUtils, locationUtils, serverUtils, cdnService, serverService, 
statusService, propertiesModel, messageModel) {
-
-    let serversTable;
-
-    var getStatuses = function() {
-        statusService.getStatuses()
-            .then(function(result) {
-                $scope.statuses = result;
-            });
-    };
-
-    var queueServerUpdates = function(server) {
-        serverService.queueServerUpdates(server.id)
-            .then(
-                function() {
-                    $scope.refresh();
-                }
-            );
-    };
-
-    var clearServerUpdates = function(server) {
-        serverService.clearServerUpdates(server.id)
-            .then(
-                function() {
-                    $scope.refresh();
-                }
-            );
-    };
-
-    var queueCDNServerUpdates = function(cdnId) {
-        cdnService.queueServerUpdates(cdnId)
-            .then(
-                function() {
-                    $scope.refresh();
-                }
-            );
-    };
-
-    var clearCDNServerUpdates = function(cdnId) {
-        cdnService.clearServerUpdates(cdnId)
-            .then(
-                function() {
-                    $scope.refresh();
-                }
-            );
-    };
-
-    var confirmDelete = function(server) {
-        var params = {
-            title: 'Delete Server: ' + server.hostName,
-            key: server.hostName
-        };
-        var 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() {
-            deleteServer(server);
-        }, function () {
-            // do nothing
-        });
-    };
-
-    var deleteServer = function(server) {
-        serverService.deleteServer(server.id)
-            .then(function(result) {
-                messageModel.setMessages(result.alerts, false);
-                $scope.refresh();
-            });
-    };
-
-    var confirmStatusUpdate = function(server) {
-        var modalInstance = $uibModal.open({
-            templateUrl: 
'common/modules/dialog/select/status/dialog.select.status.tpl.html',
-            controller: 'DialogSelectStatusController',
-            size: 'md',
-            resolve: {
-                server: function() {
-                    return server;
-                },
-                statuses: function() {
-                    return $scope.statuses;
-                }
-            }
-        });
-        modalInstance.result.then(function(status) {
-            updateStatus(status, server);
-        }, function () {
-            // do nothing
-        });
-    };
-
-    var updateStatus = function(status, server) {
-        serverService.updateStatus(server.id, { status: status.id, 
offlineReason: status.offlineReason })
-            .then(
-                function(result) {
-                    messageModel.setMessages(result.data.alerts, false);
-                    $scope.refresh();
-                },
-                function(fault) {
-                    messageModel.setMessages(fault.data.alerts, false);
-                }
-            );
-    };
-
-    $scope.servers = servers;
-
-    $scope.columns = [
-        { "name": "Cache Group", "visible": true, "searchable": true },
-        { "name": "CDN", "visible": true, "searchable": true },
-        { "name": "Domain", "visible": true, "searchable": true },
-        { "name": "Host", "visible": true, "searchable": true },
-        { "name": "HTTPS Port", "visible": false, "searchable": false },
-        { "name": "ID", "visible": false, "searchable": false },
-        { "name": "ILO IP Address", "visible": true, "searchable": true },
-        { "name": "ILO IP Gateway", "visible": false, "searchable": false },
-        { "name": "ILO IP Netmask", "visible": false, "searchable": false },
-        { "name": "ILO Username", "visible": false, "searchable": false },
-        { "name": "Interface Name", "visible": false, "searchable": false },
-        { "name": "IPv6 Address", "visible": true, "searchable": true },
-        { "name": "IPv6 Gateway", "visible": false, "searchable": false },
-        { "name": "Last Updated", "visible": false, "searchable": false },
-        { "name": "Mgmt IP Address", "visible": false, "searchable": false },
-        { "name": "Mgmt IP Gateway", "visible": false, "searchable": false },
-        { "name": "Mgmt IP Netmask", "visible": false, "searchable": false },
-        { "name": "Network Gateway", "visible": false, "searchable": false },
-        { "name": "Network IP", "visible": true, "searchable": true },
-        { "name": "Network MTU", "visible": false, "searchable": false },
-        { "name": "Network Subnet", "visible": false, "searchable": false },
-        { "name": "Offline Reason", "visible": false, "searchable": false },
-        { "name": "Phys Location", "visible": true, "searchable": true },
-        { "name": "Profile", "visible": true, "searchable": true },
-        { "name": "Rack", "visible": false, "searchable": false },
-        { "name": "Reval Pending", "visible": false, "searchable": false },
-        { "name": "Router Hostname", "visible": false, "searchable": false },
-        { "name": "Router Port Name", "visible": false, "searchable": false },
-        { "name": "Status", "visible": true, "searchable": true },
-        { "name": "TCP Port", "visible": false, "searchable": false },
-        { "name": "Type", "visible": true, "searchable": true },
-        { "name": "Update Pending", "visible": true, "searchable": true }
-    ];
-
-    $scope.contextMenuItems = [
-        {
-            text: 'Open in New Tab',
-            click: function ($itemScope) {
-                $window.open('/#!/servers/' + $itemScope.s.id, '_blank');
-            }
-        },
-        null, // Divider
-        {
-            text: 'Navigate to Server FQDN',
-            click: function ($itemScope) {
-                $window.open('http://' + $itemScope.s.hostName + '.' + 
$itemScope.s.domainName, '_blank');
-            }
-        },
-        null, // Divider
-        {
-            text: 'Edit',
-            click: function ($itemScope) {
-                $scope.editServer($itemScope.s.id);
-            }
-        },
-        {
-            text: 'Delete',
-            click: function ($itemScope) {
-                confirmDelete($itemScope.s);
-            }
-        },
-        null, // Divider
-        {
-            text: 'Update Status',
-            click: function ($itemScope) {
-                confirmStatusUpdate($itemScope.s);
-            }
-        },
-        {
-            text: 'Queue Server Updates',
-            displayed: function ($itemScope) {
-                return serverUtils.isCache($itemScope.s) && 
!$itemScope.s.updPending;
-            },
-            click: function ($itemScope) {
-                queueServerUpdates($itemScope.s);
-            }
-        },
-        {
-            text: 'Clear Server Updates',
-            displayed: function ($itemScope) {
-                return serverUtils.isCache($itemScope.s) && 
$itemScope.s.updPending;
-            },
-            click: function ($itemScope) {
-                clearServerUpdates($itemScope.s);
-            }
-        },
-        {
-            text: 'Show Charts',
-            displayed: function () {
-                return propertiesModel.properties.servers.charts.show;
-            },
-            hasBottomDivider: function () {
-                return true;
-            },
-            hasTopDivider: function () {
-                return true;
-            },
-            click: function ($itemScope) {
-                $window.open(propertiesModel.properties.servers.charts.baseUrl 
+ $itemScope.s.hostName, '_blank');
-            }
-        },
-        {
-            text: 'Manage Capabilities',
-            displayed: function ($itemScope) {
-                return serverUtils.isCache($itemScope.s);
-            },
-            hasTopDivider: function () {
-                return true;
-            },
-            click: function ($itemScope) {
-                locationUtils.navigateToPath('/servers/' + $itemScope.s.id + 
'/capabilities');
-            }
-        },
-        {
-            text: 'Manage Delivery Services',
-            displayed: function ($itemScope) {
-                return serverUtils.isEdge($itemScope.s) || 
serverUtils.isOrigin($itemScope.s);
-            },
-            hasTopDivider: function ($itemScope) {
-                return !serverUtils.isCache($itemScope.s);
-            },
-            click: function ($itemScope) {
-                locationUtils.navigateToPath('/servers/' + $itemScope.s.id + 
'/delivery-services');
-            }
-        },
-        {
-            text: 'View Config Files',
-            displayed: function ($itemScope) {
-                return serverUtils.isCache($itemScope.s);
-            },
-            click: function ($itemScope) {
-                locationUtils.navigateToPath('/servers/' + $itemScope.s.id + 
'/config-files');
-            }
-        }
-    ];
-
-    $scope.editServer = function(id) {
-        locationUtils.navigateToPath('/servers/' + id);
-    };
-
-    $scope.createServer = function() {
-        locationUtils.navigateToPath('/servers/new');
-    };
-
-    $scope.confirmCDNQueueServerUpdates = function(cdn) {
-        var params;
-        if (cdn) {
-            params = {
-                title: 'Queue Server Updates: ' + cdn.name,
-                message: 'Are you sure you want to queue server updates for 
all ' + cdn.name + ' servers?'
-            };
-            var modalInstance = $uibModal.open({
-                templateUrl: 
'common/modules/dialog/confirm/dialog.confirm.tpl.html',
-                controller: 'DialogConfirmController',
-                size: 'md',
-                resolve: {
-                    params: function () {
-                        return params;
-                    }
-                }
-            });
-            modalInstance.result.then(function() {
-                queueCDNServerUpdates(cdn.id);
-            }, function () {
-                // do nothing
-            });
-        } else {
-            params = {
-                title: 'Queue Server Updates',
-                message: "Please select a CDN"
-            };
-            var modalInstance = $uibModal.open({
-                templateUrl: 
'common/modules/dialog/select/dialog.select.tpl.html',
-                controller: 'DialogSelectController',
-                size: 'md',
-                resolve: {
-                    params: function () {
-                        return params;
-                    },
-                    collection: function(cdnService) {
-                        return cdnService.getCDNs();
-                    }
-                }
-            });
-            modalInstance.result.then(function(cdn) {
-                queueCDNServerUpdates(cdn.id);
-            }, function () {
-                // do nothing
-            });
-        }
-    };
-
-    $scope.confirmCDNClearServerUpdates = function(cdn) {
-        var params;
-        if (cdn) {
-            params = {
-                title: 'Clear Server Updates: ' + cdn.name,
-                message: 'Are you sure you want to clear server updates for 
all ' + cdn.name + ' servers?'
-            };
-            var modalInstance = $uibModal.open({
-                templateUrl: 
'common/modules/dialog/confirm/dialog.confirm.tpl.html',
-                controller: 'DialogConfirmController',
-                size: 'md',
-                resolve: {
-                    params: function () {
-                        return params;
-                    }
-                }
-            });
-            modalInstance.result.then(function() {
-                clearCDNServerUpdates(cdn.id);
-            }, function () {
-                // do nothing
-            });
-
-
-        } else {
-            params = {
-                title: 'Clear Server Updates',
-                message: "Please select a CDN"
-            };
-            var modalInstance = $uibModal.open({
-                templateUrl: 
'common/modules/dialog/select/dialog.select.tpl.html',
-                controller: 'DialogSelectController',
-                size: 'md',
-                resolve: {
-                    params: function () {
-                        return params;
-                    },
-                    collection: function(cdnService) {
-                        return cdnService.getCDNs();
-                    }
-                }
-            });
-            modalInstance.result.then(function(cdn) {
-                clearCDNServerUpdates(cdn.id);
-            }, function () {
-                // do nothing
-            });
-        }
-    };
-
-    $scope.refresh = function() {
-        $state.reload(); // reloads all the resolves for the view
-    };
-
-    $scope.toggleVisibility = function(colName) {
-        const col = serversTable.column(colName + ':name');
-        col.visible(!col.visible());
-        serversTable.rows().invalidate().draw();
-    };
-
-    $scope.ssh = serverUtils.ssh;
-
-    $scope.isOffline = serverUtils.isOffline;
-
-    $scope.offlineReason = serverUtils.offlineReason;
-
-    $scope.getRelativeTime = dateUtils.getRelativeTime;
-
-    $scope.navigateToPath = locationUtils.navigateToPath;
-
-    var init = function () {
-        getStatuses();
-    };
-    init();
-
-    angular.element(document).ready(function () {
-        serversTable = $('#serversTable').DataTable({
-            "lengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
-            "iDisplayLength": 25,
-            "aaSorting": [],
-            "columns": $scope.columns,
-            "initComplete": function(settings, json) {
-                try {
-                    // need to create the show/hide column checkboxes and bind 
to the current visibility
-                    $scope.columns = 
JSON.parse(localStorage.getItem('DataTables_serversTable_/')).columns;
-                } catch (e) {
-                    console.error("Failure to retrieve required column info 
from localStorage (key=DataTables_serversTable_/):", e);
-                }
-            }
-        });
-    });
+var TableServersController = function(servers, $scope, $state, $uibModal, 
$window, dateUtils, locationUtils, serverUtils, cdnService, serverService, 
statusService, propertiesModel, messageModel, userModel, $document) {
+       /**** Table cell formatters/renderers ****/
 
+       // browserify can't handle classes...
+       function SSHCellRenderer() {}
+       SSHCellRenderer.prototype.init = function(params) {
+               this.eGui = document.createElement("A");
+               this.eGui.href = "ssh://" + userModel.user.username + "@" + 
params.value;
+               this.eGui.setAttribute("target", "_blank");
+               this.eGui.textContent = params.value;
+       };
+       SSHCellRenderer.prototype.getGui = function() {return this.eGui;};
+
+       function UpdateCellRenderer() {}
+       UpdateCellRenderer.prototype.init = function(params) {
+               this.eGui = document.createElement("I");
+               this.eGui.setAttribute("aria-hidden", "true");
+               this.eGui.setAttribute("title", String(params.value));
+               this.eGui.classList.add("fa", "fa-lg");
+               if (params.value) {
+                       this.eGui.classList.add("fa-clock-o");
+               } else {
+                       this.eGui.classList.add("fa-check");
+               }
+       }
+       UpdateCellRenderer.prototype.getGui = function() {return this.eGui;};
+
+       /**
+        * Gets text with which to file a status tooltip.
+        * @returns {string | undefined} The offline reason if the server is 
offline, otherwise nothing.
+        */
+       function offlineReasonTooltip(params) {
+               if (!params.value || !serverUtils.isOffline(params.value)) {
+                       return;
+               }
+               return params.data.offlineReason;
+       }
+
+       /**
+        * Formats the contents of a 'lastUpdated' column cell as "relative to 
now".
+        */
+       function dateCellFormatter(params) {
+               return dateUtils.getRelativeTime(params.value);
+       }
+
+
+       /**** Constants, scope data, etc. ****/
+
+       /** The columns of the ag-grid table */
+       const columns = [
+               {
+                       headerName: "Cache Group",
+                       field: "cachegroup",
+                       hide: false,
+               },
+               {
+                       headerName: "CDN",
+                       field: "cdnName",
+                       hide: false,
+               },
+               {
+                       headerName: "Domain",
+                       field: "domainName",
+                       hide: false,
+               },
+               {
+                       headerName: "Host",
+                       field: "hostName",
+                       hide: false,
+               },
+               {
+                       headerName: "HTTPS Port",
+                       field: "httpsPort",
+                       hide: true,
+                       filter: "agNumberColumnFilter"
+               },
+               {
+                       headerName: "ID",
+                       field: "id",
+                       hide: true,
+                       filter: "agNumberColumnFilter"
+               },
+               {
+                       headerName: "ILO IP Address",
+                       field: "iloIpAddress",
+                       hide: true,
+                       cellRenderer: "sshCellRenderer",
+                       onCellClicked: null
+               },
+               {
+                       headerName: "ILO IP Gateway",
+                       field: "iloIpGateway",
+                       hide: true,
+                       cellRenderer: "sshCellRenderer",
+                       onCellClicked: null
+               },
+               {
+                       headerName: "ILO IP Netmask",
+                       field: "iloIpNetmask",
+                       hide: true,
+               },
+               {
+                       headerName: "ILO Username",
+                       field: "iloUsername",
+                       hide: true,
+               },
+               {
+                       headerName: "Interface Name",
+                       field: "interfaceName",
+                       hide: true,
+               },
+               {
+                       headerName: "IPv6 Address",
+                       field: "ipv6Address",
+                       hide: false,
+               },
+               {
+                       headerName: "IPv6 Gateway",
+                       field: "ipv6Gateway",
+                       hide: true,
+               },
+               {
+                       headerName: "Last Updated",
+                       field: "lastUpdated",
+                       hide: true,
+                       filter: "agDateColumnFilter",
+                       valueFormatter: dateCellFormatter
+               },
+               {
+                       headerName: "Mgmt IP Address",
+                       field: "mgmtIpAddress",
+                       hide: true,
+               },
+               {
+                       headerName: "Mgmt IP Gateway",
+                       field: "mgmtIpGateway",
+                       hide: true,
+                       filter: true,
+                       cellRenderer: "sshCellRenderer",
+                       onCellClicked: null
+               },
+               {
+                       headerName: "Mgmt IP Netmask",
+                       field: "mgmtIpNetmask",
+                       hide: true,
+                       filter: true,
+                       cellRenderer: "sshCellRenderer",
+                       onCellClicked: null
+               },
+               {
+                       headerName: "Network Gateway",
+                       field: "ipGateway",
+                       hide: true,
+                       filter: true,
+                       cellRenderer: "sshCellRenderer",
+                       onCellClicked: null
+               },
+               {
+                       headerName: "Network IP",
+                       field: "ipAddress",
+                       hide: false,
+                       filter: true,
+                       cellRenderer: "sshCellRenderer",
+                       onCellClicked: null
+               },
+               {
+                       headerName: "Network MTU",
+                       field: "interfaceMtu",
+                       hide: true,
+                       filter: "agNumberColumnFilter"
+               },
+               {
+                       headerName: "Network Subnet",
+                       field: "ipNetmask",
+                       hide: true,
+               },
+               {
+                       headerName: "Offline Reason",
+                       field: "offlineReason",
+                       hide: true,
+               },
+               {
+                       headerName: "Phys Location",
+                       field: "physLocation",
+                       hide: true,
+               },
+               {
+                       headerName: "Profile",
+                       field: "profile",
+                       hide: false,
+               },
+               {
+                       headerName: "Rack",
+                       field: "rack",
+                       hide: true,
+               },
+               {
+                       headerName: "Reval Pending",
+                       field: "revalPending",
+                       hide: true,
+                       filter: true,
+                       cellRenderer: "updateCellRenderer"
+               },
+               {
+                       headerName: "Router Hostname",
+                       field: "routerHostName",
+                       hide: true,
+               },
+               {
+                       headerName: "Router Port Name",
+                       field: "routerPortName",
+                       hide: true,
+               },
+               {
+                       headerName: "Status",
+                       field: "status",
+                       hide: false,
+                       tooltip: offlineReasonTooltip
+               },
+               {
+                       headerName: "TCP Port",
+                       field: "tcpPort",
+                       hide: true,
+               },
+               {
+                       headerName: "Type",
+                       field: "type",
+                       hide: false,
+               },
+               {
+                       headerName: "Update Pending",
+                       field: "updPending",
+                       hide: false,
+                       filter: true,
+                       cellRenderer: "updateCellRenderer"
+               }
+       ];
+
+       /** All of the statuses (populated on init). */
+       let statuses = [];
+
+       /** All of the servers - lastUpdated fields converted to actual Dates. 
*/
+       $scope.servers = servers.map(function(x){x.lastUpdated = x.lastUpdated 
? new Date(x.lastUpdated.replace("+00", "Z")) : x.lastUpdated;});
+
+       /** The base URL to use for constructing links to server charts. */
+       $scope.chartsBase = propertiesModel.properties.servers.charts.baseUrl;
+
+       /** The currently selected server - at the moment only used by the 
context menu */
+       $scope.server = {
+               hostName: "",
+               domainName: "",
+               id: -1
+       };
+
+       /** Options, configuration, data and callbacks for the ag-grid table. */
+       $scope.gridOptions = {
+               components: {
+                       sshCellRenderer: SSHCellRenderer,
+                       updateCellRenderer: UpdateCellRenderer
+               },
+               columnDefs: columns,
+               defaultColDef: {
+                       filter: true,
+                       onCellClicked: function(params) {
+                                       
locationUtils.navigateToPath('/servers/' + params.data.id);
+                                       // Event is outside the digest cycle, 
so we need to trigger one.
+                                       $scope.$apply();
+                               },
+                       sortable: true,
+                       resizable: true
+               },
+               rowData: servers,
+               pagination: true,
+               rowBuffer: 0,
+               onColumnResized: function(params) {
+                       localStorage.setItem("servers_table_columns", 
JSON.stringify($scope.gridOptions.columnApi.getColumnState()));
+               },
+               tooltipShowDelay: 500,
+               allowContextMenuWithControlKey: true,
+               preventDefaultOnContextMenu: true,
+               onCellContextMenu: function(params) {
+                       $scope.showMenu = true;
+                       $scope.menuStyle.left = String(params.event.pageX) + 
"px";
+                       $scope.menuStyle.top = String(params.event.pageY) + 
"px";
+                       $scope.server = params.data;
+                       $scope.$apply();
+               },
+               colResizeDefault: "shift"
+       };
+
+       /** These three functions are used by the context menu to determine 
what functionality to provide for a server. */
+       $scope.isCache = serverUtils.isCache;
+       $scope.isEdge = serverUtils.isEdge;
+       $scope.isOrigin = serverUtils.isOrigin;
+
+       /** Used by the context menu to determine whether or not to include 
links to server charts. */
+       $scope.showCharts = propertiesModel.properties.servers.charts.show;
+
+       /** This is used to position the context menu under the cursor. */
+       $scope.menuStyle = {
+               left: 0,
+               top: 0,
+       };
+
+       /** Controls whether or not the context menu is visible. */
+       $scope.showMenu = false;
+
+
+       /**** Miscellaneous scope functions ****/
+
+       /** Reloads all 'resolve'd data for the view. */
+       $scope.refresh = function() {
+               $state.reload();
+       };
+
+       /** Toggles the visibility of a column that has the ID provided as 
'col'. */
+       $scope.toggleVisibility = function(col) {
+               const visible = 
$scope.gridOptions.columnApi.getColumn(col).isVisible();
+               $scope.gridOptions.columnApi.setColumnVisible(col, !visible);
+       };
+
+       /** Downloads the table as a CSV */
+       $scope.exportCSV = function() {
+               // TODO: figure out how to reconcile clicking on a server 
taking you to it
+               // with row selection exports.
+               const params = {
+                       allColumns: true,
+                       fileName: "servers.csv",
+               };
+               $scope.gridOptions.api.exportDataAsCsv(params);
+       }
+
+       /**** Context menu functions ****/
+
+       $scope.queueServerUpdates = function(server, event) {
+               event.stopPropagation();
+               
serverService.queueServerUpdates(server.id).then($scope.refresh);
+       };
+
+       $scope.clearServerUpdates = function(server, event) {
+               event.stopPropagation();
+               
serverService.clearServerUpdates(server.id).then($scope.refresh);
+       };
+
+       $scope.confirmDelete = function(server, event) {
+               event.stopPropagation();
+
+               const params = {
+                       title: 'Delete Server: ' + server.hostName,
+                       key: server.hostName
+               };
+               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() {
+                               serverService.deleteServer(server.id).then(
+                                       function(result) {
+                                               
messageModel.setMessages(result.alerts, false);
+                                               $scope.refresh();
+                                       },
+                                       function(err) {
+                                               // TODO: use template strings 
once the build can handle them.
+                                               console.error("Error deleting 
server", server.hostName + "." + server.domainName, "(#" + String(server.id) + 
"):", err);
+                                       }
+                               );
+                       },
+                       function() {
+                               // This is just a cancel event from closing the 
dialog, do nothing.
+                       }
+               );
+       };
+
+       /**
+        * updateStatus sets the status of the given server to the given status 
value.
+        *
+        * @param {{id: number, offlineReason?: string}} status The numeric ID 
of the status to set along with a reason why it was set offline, if applicable.
+        * @param {{id: number}} server The server (or at least its numeric ID) 
which will have its status set.
+        */
+       function updateStatus(status, server) {
+               const params = {
+                       status: status.id,
+                       offlineReason: status.offlineReason
+               };
+
+               serverService.updateStatus(server.id, params).then(
+                       function(result) {
+                               messageModel.setMessages(result.data.alerts, 
false);
+                               $scope.refresh();
+                       },
+                       function(fault) {
+                               messageModel.setMessages(fault.data.alerts, 
false);
+                       }
+               );
+       };
+
+       $scope.confirmStatusUpdate = function(server, event) {
+               event.stopPropagation();
+
+               const modalInstance = $uibModal.open({
+                       templateUrl: 
'common/modules/dialog/select/status/dialog.select.status.tpl.html',
+                       controller: 'DialogSelectStatusController',
+                       size: 'md',
+                       resolve: {
+                               server: function() {
+                                       return server;
+                               },
+                               statuses: function() {
+                                       return statuses;
+                               }
+                       }
+               });
+               modalInstance.result.then(
+                       function(status) {
+                               updateStatus(status, server);
+                       },
+                       function () {
+                               // this is just a cancel event from closing the 
dialog, do nothing
+                       }
+               );
+       };
+
+       $scope.confirmCDNQueueServerUpdates = function(cdn) {
+               let params;
+               if (cdn) {
+                       params = {
+                               title: 'Queue Server Updates: ' + cdn.name,
+                               message: 'Are you sure you want to queue server 
updates for all ' + cdn.name + ' servers?'
+                       };
+                       const modalInstance = $uibModal.open({
+                               templateUrl: 
'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+                               controller: 'DialogConfirmController',
+                               size: 'md',
+                               resolve: {
+                                       params: function () {
+                                               return params;
+                                       }
+                               }
+                       });
+                       modalInstance.result.then(function() {
+                               
cdnService.queueServerUpdates(cdn.id).then($scope.refresh);
+                       }, function () {
+                               // this is just a cancel event from closing the 
dialog, do nothing
+                       });
+               } else {
+                       params = {
+                               title: 'Queue Server Updates',
+                               message: "Please select a CDN"
+                       };
+                       const modalInstance = $uibModal.open({
+                               templateUrl: 
'common/modules/dialog/select/dialog.select.tpl.html',
+                               controller: 'DialogSelectController',
+                               size: 'md',
+                               resolve: {
+                                       params: function () {
+                                               return params;
+                                       },
+                                       collection: function(cdnService) {
+                                               return cdnService.getCDNs();
+                                       }
+                               }
+                       });
+                       modalInstance.result.then(function(cdn) {
+                               
cdnService.queueServerUpdates(cdn.id).then($scope.refresh);
+                       }, function () {
+                               // do nothing
+                       });
+               }
+       };
+
+       $scope.confirmCDNClearServerUpdates = function(cdn) {
+               let params;
+               if (cdn) {
+                       params = {
+                               title: 'Clear Server Updates: ' + cdn.name,
+                               message: 'Are you sure you want to clear server 
updates for all ' + cdn.name + ' servers?'
+                       };
+                       const modalInstance = $uibModal.open({
+                               templateUrl: 
'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+                               controller: 'DialogConfirmController',
+                               size: 'md',
+                               resolve: {
+                                       params: function () {
+                                               return params;
+                                       }
+                               }
+                       });
+                       modalInstance.result.then(function() {
+                               
cdnService.clearServerUpdates(cdn.id).then($scope.refresh);
+                       }, function () {
+                               // do nothing
+                       });
+
+
+               } else {
+                       params = {
+                               title: 'Clear Server Updates',
+                               message: "Please select a CDN"
+                       };
+                       const modalInstance = $uibModal.open({
+                               templateUrl: 
'common/modules/dialog/select/dialog.select.tpl.html',
+                               controller: 'DialogSelectController',
+                               size: 'md',
+                               resolve: {
+                                       params: function () {
+                                               return params;
+                                       },
+                                       collection: function(cdnService) {
+                                               return cdnService.getCDNs();
+                                       }
+                               }
+                       });
+                       modalInstance.result.then(function(cdn) {
+                               
cdnService.clearServerUpdates(cdn.id).then($scope.refresh);
+                       }, function () {
+                               // do nothing
+                       });
+               }
+       };
+
+
+       /**** Initialization code, including loading user columns from 
localstorage ****/
+       angular.element(document).ready(function () {
+               try {
+                       // need to create the show/hide column checkboxes and 
bind to the current visibility
+                       const colstates = 
JSON.parse(localStorage.getItem("servers_table_columns"));
+                       if (colstates) {
+                               if 
(!$scope.gridOptions.columnApi.setColumnState(colstates)) {
+                                       console.error("Failed to load stored 
column state: one or more columns not found");
+                               }
+                       } else {
+                               $scope.gridOptions.api.sizeColumnsToFit();
+                       }
+               } catch (e) {
+                       console.error("Failure to retrieve required column info 
from localStorage (key=servers_table_columns):", e);
+               }
+
+               try {
+                       const filterState = 
JSON.parse(localStorage.getItem("servers_table_filters"));
+                       $scope.gridOptions.api.setFilterModel(filterState);
+               } catch (e) {
+                       console.error("Failure to load stored filter state:", 
e);
+               }
+
+               $scope.gridOptions.api.addEventListener("filterChanged", 
function() {
+                       localStorage.setItem("servers_table_filters", 
JSON.stringify($scope.gridOptions.api.getFilterModel()));
+               });
+
+               try {
+                       const sortState = 
JSON.parse(localStorage.getItem("servers_table_sort"));
+                       $scope.gridOptions.api.setSortModel(sortState);
+               } catch (e) {
+                       console.error("Failure to load stored sort state:", e);
+               }
+
+               $scope.gridOptions.api.addEventListener("sortChanged", 
function() {
+                       localStorage.setItem("servers_table_sort", 
JSON.stringify($scope.gridOptions.api.getSortModel()));
+               });
+
+               $scope.gridOptions.api.addEventListener("columnMoved", 
function() {
+                       localStorage.setItem("servers_table_columns", 
JSON.stringify($scope.gridOptions.columnApi.getColumnState()));
+               });
+
+               $scope.gridOptions.api.addEventListener("columnVisible", 
function() {
+                       $scope.gridOptions.api.sizeColumnsToFit();
+                       try {
+                               colStates = 
$scope.gridOptions.columnApi.getColumnState();
+                               localStorage.setItem("servers_table_columns", 
JSON.stringify(colStates));
+                       } catch (e) {
+                               console.error("Failed to store column defs to 
local storage:", e);
+                       }
+               });
+
+               // clicks outside the context menu will hide it
+               $document.bind("click", function(e) {
+                       $scope.showMenu = false;
+                       e.stopPropagation();
+                       $scope.$apply();
+               });
+
+               statusService.getStatuses().then(
+                       function(result) {
+                               statuses = result;
+                       }
+               );
+       });
 };
 
-TableServersController.$inject = ['servers', '$scope', '$state', '$uibModal', 
'$window', 'dateUtils', 'locationUtils', 'serverUtils', 'cdnService', 
'serverService', 'statusService', 'propertiesModel', 'messageModel'];
+TableServersController.$inject = ['servers', '$scope', '$state', '$uibModal', 
'$window', 'dateUtils', 'locationUtils', 'serverUtils', 'cdnService', 
'serverService', 'statusService', 'propertiesModel', 'messageModel', 
"userModel", "$document"];
 module.exports = TableServersController;
diff --git a/traffic_portal/app/src/common/modules/table/servers/index.js 
b/traffic_portal/app/src/common/modules/table/servers/index.js
index e955714..2b5d9b5 100644
--- a/traffic_portal/app/src/common/modules/table/servers/index.js
+++ b/traffic_portal/app/src/common/modules/table/servers/index.js
@@ -18,4 +18,5 @@
  */
 
 module.exports = angular.module('trafficPortal.table.servers', [])
-    .controller('TableServersController', require('./TableServersController'));
+    .controller('TableServersController', require('./TableServersController'))
+    .controller('TableParentServersController', 
require('./TableParentServersController'));
diff --git 
a/traffic_portal/app/src/common/modules/table/servers/table.servers.tpl.html 
b/traffic_portal/app/src/common/modules/table/servers/table.servers.tpl.html
index 9c3dce4..4d36373 100644
--- a/traffic_portal/app/src/common/modules/table/servers/table.servers.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/servers/table.servers.tpl.html
@@ -23,7 +23,7 @@ under the License.
             <li class="active">Servers</li>
         </ol>
         <div class="pull-right">
-            <button class="btn btn-primary" name="createServersButton" 
title="Create Server" ng-click="createServer()"><i class="fa 
fa-plus"></i></button>
+            <a class="btn btn-primary" name="createServersButton" 
title="Create Server" href="#!/servers/new"><i class="fa fa-plus"></i></a>
             <button class="btn btn-default" title="Refresh" 
ng-click="refresh()"><i class="fa fa-refresh"></i></button>
             <div id="toggleColumns" class="btn-group" role="group" 
title="Select Table Columns" uib-dropdown is-open="columnSettings.isopen">
                 <button type="button" class="btn btn-default dropdown-toggle" 
uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
@@ -31,9 +31,9 @@ under the License.
                     <span class="caret"></span>
                 </button>
                 <menu ng-click="$event.stopPropagation()" 
class="column-settings dropdown-menu-right dropdown-menu" uib-dropdown-menu>
-                    <li role="menuitem" ng-repeat="c in columns | 
orderBy:'name'">
+                    <li role="menuitem" ng-repeat="c in 
gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
                         <div class="checkbox">
-                            <label><input type="checkbox" ng-model="c.visible" 
ng-click="toggleVisibility(c.name)"> {{::c.name}}</label>
+                            <label><input type="checkbox" 
ng-checked="c.isVisible()" 
ng-click="toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
                         </div>
                     </li>
                 </menu>
@@ -46,99 +46,53 @@ under the License.
                 <ul class="dropdown-menu-right dropdown-menu" 
uib-dropdown-menu>
                     <li role="menuitem"><a 
ng-click="confirmCDNQueueServerUpdates(null)">Queue CDN Server Updates</a></li>
                     <li role="menuitem"><a 
ng-click="confirmCDNClearServerUpdates(null)">Clear CDN Server Updates</a></li>
+                    <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="exportCSV()">Export CSV</button></li>
                 </ul>
             </div>
         </div>
         <div class="clearfix"></div>
     </div>
     <div class="x_content">
-        <br>
-        <table id="serversTable" class="table responsive-utilities 
jambo_table">
-            <thead>
-                <tr class="headings">
-                    <th>Cache Group</th>
-                    <th>CDN</th>
-                    <th>Domain</th>
-                    <th>Host</th>
-                    <th>HTTPS Port</th>
-                    <th>ID</th>
-                    <th>ILO IP Address</th>
-                    <th>ILO IP Gateway</th>
-                    <th>ILO IP Netmask</th>
-                    <th>ILO Username</th>
-                    <th>Interface Name</th>
-                    <th>IPv6 Address</th>
-                    <th>IPv6 Gateway</th>
-                    <th>Last Updated</th>
-                    <th>Mgmt IP Address</th>
-                    <th>Mgmt IP Gateway</th>
-                    <th>Mgmt IP Netmask</th>
-                    <th>Network Gateway</th>
-                    <th>Network IP</th>
-                    <th>Network MTU</th>
-                    <th>Network Subnet</th>
-                    <th>Offline Reason</th>
-                    <th>Phys Location</th>
-                    <th>Profile</th>
-                    <th>Rack</th>
-                    <th>Reval Pending</th>
-                    <th>Router Hostname</th>
-                    <th>Router Port Name</th>
-                    <th>Status</th>
-                    <th>TCP Port</th>
-                    <th>Type</th>
-                    <th>Update Pending</th>
-                </tr>
-            </thead>
-            <tbody>
-                <tr ng-click="editServer(s.id)" ng-repeat="s in ::servers" 
ng-class="::{'active': s.updPending}" context-menu="contextMenuItems">
-                    <td 
data-search="^{{::s.cachegroup}}$">{{::s.cachegroup}}</td>
-                    <td data-search="^{{::s.cdnName}}$">{{::s.cdnName}}</td>
-                    <td 
data-search="^{{::s.domainName}}$">{{::s.domainName}}</td>
-                    <td name="hostName" 
data-search="^{{::s.hostName}}$">{{::s.hostName}}</td>
-                    <td 
data-search="^{{::s.httpsPort}}$">{{::s.httpsPort}}</td>
-                    <td data-search="^{{::s.id}}$">{{::s.id}}</td>
-                    <td data-search="^{{::s.iloIpAddress}}$"><a 
ng-click="ssh(s.iloIpAddress, $event)">{{::s.iloIpAddress}}</a></td>
-                    <td data-search="^{{::s.iloIpGateway}}$"><a 
ng-click="ssh(s.iloIpGateway, $event)">{{::s.iloIpGateway}}</a></td>
-                    <td 
data-search="^{{::s.iloIpNetmask}}$">{{::s.iloIpNetmask}}</td>
-                    <td 
data-search="^{{::s.iloUsername}}$">{{::s.iloUsername}}</td>
-                    <td 
data-search="^{{::s.interfaceName}}$">{{::s.interfaceName}}</td>
-                    <td 
data-search="^{{::s.ip6Address}}$">{{::s.ip6Address}}</td>
-                    <td 
data-search="^{{::s.ip6Gateway}}$">{{::s.ip6Gateway}}</td>
-                    <td data-search="^{{::getRelativeTime(s.lastUpdated)}}$" 
data-order="{{::s.lastUpdated}}">{{::getRelativeTime(s.lastUpdated)}}</td>
-                    <td data-search="^{{::s.mgmtIpAddress}}$"><a 
ng-click="ssh(s.mgmtIpAddress, $event)">{{::s.mgmtIpAddress}}</a></td>
-                    <td data-search="^{{::s.mgmtIpGateway}}$"><a 
ng-click="ssh(s.mgmtIpGateway, $event)">{{::s.mgmtIpGateway}}</a></td>
-                    <td 
data-search="^{{::s.mgmtIpNetmask}}$">{{::s.mgmtIpNetmask}}</td>
-                    <td data-search="^{{::s.ipGateway}}$"><a 
ng-click="ssh(s.ipGateway, $event)">{{::s.ipGateway}}</a></td>
-                    <td data-search="^{{::s.ipAddress}}$"><a 
ng-click="ssh(s.ipAddress, $event)">{{::s.ipAddress}}</a></td>
-                    <td 
data-search="^{{::s.interfaceMtu}}$">{{::s.interfaceMtu}}</td>
-                    <td 
data-search="^{{::s.ipNetmask}}$">{{::s.ipNetmask}}</td>
-                    <td 
data-search="^{{::s.offlineReason}}$">{{::s.offlineReason}}</td>
-                    <td 
data-search="^{{::s.physLocation}}$">{{::s.physLocation}}</td>
-                    <td data-search="^{{::s.profile}}$">{{::s.profile}}</td>
-                    <td data-search="^{{::s.rack}}$">{{::s.rack}}</td>
-                    <td data-search="{{(s.revalPending) ? 'RVL' : ''}}" 
data-order="{{::s.revalPending}}">
-                        <i title="Reval Pending (RVL)" 
ng-show="s.revalPending" class="fa fa-clock-o fa-lg" aria-hidden="true"></i>
-                        <i title="Reval Applied" ng-show="!s.revalPending" 
class="fa fa-check fa-lg" aria-hidden="true"></i>
-                    </td>
-                    <td 
data-search="^{{::s.routerHostName}}$">{{::s.routerHostName}}</td>
-                    <td 
data-search="^{{::s.routerPortName}}$">{{::s.routerPortName}}</td>
-                    <td data-search="^{{::s.status}}$">
-                        <span 
ng-if="!isOffline(s.status)">{{::s.status}}</span>
-                        <span ng-if="isOffline(s.status)" 
uib-popover="{{::offlineReason(s)}}" popover-title="Offline Reason" 
popover-trigger="mouseenter" popover-placement="bottom" 
popover-append-to-body="true">{{::s.status}}</span>
-                    </td>
-                    <td data-search="^{{::s.tcpPort}}$">{{::s.tcpPort}}</td>
-                    <td data-search="^{{::s.type}}$">{{::s.type}}</td>
-                    <td data-search="{{(s.updPending) ? 'UPD' : ''}}" 
data-order="{{::s.updPending}}">
-                        <i title="Updates Pending (UPD)" 
ng-show="s.updPending" class="fa fa-clock-o fa-lg" aria-hidden="true"></i>
-                        <i title="Updates Applied" ng-show="!s.updPending" 
class="fa fa-check fa-lg" aria-hidden="true"></i>
-                    </td>
-                </tr>
-            </tbody>
-        </table>
+        <div style="height: 740px;" ag-grid="gridOptions" 
class="ag-theme-alpine"></div>
     </div>
 </div>
 
-
-
-
+<menu class="dropdown-menu" ng-style="menuStyle" type="contextmenu" 
ng-show="showMenu">
+    <ul>
+        <li role="menuitem">
+            <a ng-href="#!/servers/{{server.id}}" target="_blank">Open in New 
Tab</a>
+        </li>
+        <hr class="divider"/>
+        <li role="menuitem">
+            <a ng-href="http://{{server.hostName}}.{{server.domainName}}"; 
target="_blank">Navigate To Server FQDN</a>
+        </li>
+        <hr class="divider"/>
+        <li role="menuitem">
+            <a ng-href="#!/servers/{{server.id}}">Edit</a>
+        </li>
+        <li role="menuitem">
+            <button type="button" ng-click="confirmDelete(server, 
$event)">Delete</button>
+        </li>
+        <hr class="divider"/>
+        <li role="menuitem">
+            <button type="button" ng-click="confirmStatusUpdate(server, 
$event)">Update Status</button>
+        </li>
+        <li role="menuitem">
+            <button type="button" ng-click="queueServerUpdates(server, 
$event)" ng-disabled="!isCache(server) || server.updPending">Queue Server 
Updates</button>
+        </li>
+        <li role="menuitem">
+            <button type="button" ng-click="clearServerUpdates(server, 
$event)" ng-disabled="!isCache(server) || !server.updPending">Clear Server 
Updates</button>
+        </li>
+        <hr class="divider"/>
+        <li role="menuitem" ng-if="showCharts">
+            <a ng-href="{{chartsBase}}{{server.hostName}}" 
target="_blank">Show Charts</a>
+        </li>
+        <hr class="divider"/>
+        <li role="menuitem" ng-show="isCache(server)">
+            <a ng-href="#!/servers/{{server.id}}/capabilities">Manage 
Capabilities</a>
+        </li>
+        <li role="menuitem" ng-show="isEdge(server) || isOrigin(server)">
+            <a ng-href="#!/servers/{{server.id}}/delivery-services">Manage 
Delivery Services</a>
+        </li>
+    </ul>
+</menu>
diff --git 
a/traffic_portal/app/src/common/modules/table/statusServers/TableStatusServersController.js
 
b/traffic_portal/app/src/common/modules/table/statusServers/TableStatusServersController.js
index b35e637..b069705 100644
--- 
a/traffic_portal/app/src/common/modules/table/statusServers/TableStatusServersController.js
+++ 
b/traffic_portal/app/src/common/modules/table/statusServers/TableStatusServersController.js
@@ -19,8 +19,8 @@
 
 var TableStatusServersController = function(status, servers, $controller, 
$scope) {
 
-       // extends the TableServersController to inherit common methods
-       angular.extend(this, $controller('TableServersController', { servers: 
servers, $scope: $scope }));
+       // extends the TableParentServersController to inherit common methods
+       angular.extend(this, $controller('TableParentServersController', { 
servers: servers, $scope: $scope }));
 
        let statusServersTable;
 
diff --git 
a/traffic_portal/app/src/common/modules/table/typeServers/TableTypeServersController.js
 
b/traffic_portal/app/src/common/modules/table/typeServers/TableTypeServersController.js
index 3b03e50..2930c9d 100644
--- 
a/traffic_portal/app/src/common/modules/table/typeServers/TableTypeServersController.js
+++ 
b/traffic_portal/app/src/common/modules/table/typeServers/TableTypeServersController.js
@@ -19,8 +19,8 @@
 
 var TableTypeServersController = function(type, servers, $controller, $scope) {
 
-       // extends the TableServersController to inherit common methods
-       angular.extend(this, $controller('TableServersController', { servers: 
servers, $scope: $scope }));
+       // extends the TableParentServersController to inherit common methods
+       angular.extend(this, $controller('TableParentServersController', { 
servers: servers, $scope: $scope }));
 
        let typeServersTable;
 
diff --git a/traffic_portal/app/src/index.html 
b/traffic_portal/app/src/index.html
index 0593391..cba678e 100644
--- a/traffic_portal/app/src/index.html
+++ b/traffic_portal/app/src/index.html
@@ -49,6 +49,7 @@ under the License.
         <div class="app container body" ui-view></div>
 
         <script src="resources/assets/js/shared-libs.js"></script>
+        <script 
src="resources/assets/js/ag-grid-community/dist/ag-grid-community.min.js"></script>
         <script src="resources/assets/js/app.js"></script>
         <script src="resources/assets/js/config.js"></script>
 
diff --git a/traffic_portal/grunt/copy.js b/traffic_portal/grunt/copy.js
index f774d39..785062e 100644
--- a/traffic_portal/grunt/copy.js
+++ b/traffic_portal/grunt/copy.js
@@ -6,9 +6,9 @@
  * 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
@@ -42,6 +42,13 @@ module.exports = {
                     'traffic_portal_release.json',
                     'traffic_portal_properties.json'
                 ]
+            },
+            {
+                expand: true,
+                dot: true,
+                cwd: "<%= globalConfig.importdir %>",
+                dest: "<%= globalConfig.resourcesdir %>/assets/js/",
+                src: ["ag-grid-community/dist/ag-grid-community.min.js"]
             }
         ]
     },
@@ -78,6 +85,13 @@ module.exports = {
                     'traffic_portal_release.json',
                     'traffic_portal_properties.json'
                 ]
+            },
+            {
+                expand: true,
+                dot: true,
+                cwd: "<%= globalConfig.importdir %>",
+                dest: "<%= globalConfig.resourcesdir %>/assets/js/",
+                src: ["ag-grid-community/dist/ag-grid-community.min.js"]
             }
         ]
     }
diff --git a/traffic_portal/grunt/globalConfig.js 
b/traffic_portal/grunt/globalConfig.js
index 71ab1b4..95b34f2 100644
--- a/traffic_portal/grunt/globalConfig.js
+++ b/traffic_portal/grunt/globalConfig.js
@@ -6,9 +6,9 @@
  * 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
@@ -21,6 +21,7 @@ module.exports = function() {
     var globalConfig = {
         app: 'app',
         resourcesdir: 'app/dist/public/resources',
+        importdir: "node_modules",
         distdir: 'app/dist',
         srcserverdir: './server',
         srcdir: 'app/src',
diff --git a/traffic_portal/package-lock.json b/traffic_portal/package-lock.json
index 2ad7393..982bfbe 100644
--- a/traffic_portal/package-lock.json
+++ b/traffic_portal/package-lock.json
@@ -83,6 +83,11 @@
         }
       }
     },
+    "ag-grid-community": {
+      "version": "23.2.0",
+      "resolved": 
"https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-23.2.0.tgz";,
+      "integrity": 
"sha512-aG7Ghfu79HeqOCd50GhFSeZUX1Tw9BVUX1VKMuglkAcwYPTQjuYvYT7QVQB5FGzfFjcVq4a1QFfcgdoAcZYJIA=="
+    },
     "align-text": {
       "version": "0.1.4",
       "resolved": 
"https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz";,
diff --git a/traffic_portal/package.json b/traffic_portal/package.json
index 4462040..30d6876 100644
--- a/traffic_portal/package.json
+++ b/traffic_portal/package.json
@@ -30,5 +30,8 @@
     "requirejs": "2.3.6",
     "ssl-root-cas": "1.3.1",
     "time-grunt": "1.3.0"
+  },
+  "dependencies": {
+    "ag-grid-community": "^23.2.0"
   }
 }
diff --git 
a/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js 
b/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js
index c6b99c3..94a7021 100644
--- a/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js
+++ b/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js
@@ -114,7 +114,7 @@ describe('Traffic Portal Delivery Services Suite', 
function() {
                first.click();
                expect(first.isSelected()).toBe(false);
                let tableColumns = element.all(by.css('#deliveryServicesTable 
tr:first-child td'));
-               expect(tableColumns.count()).toBe(10);
+               expect(tableColumns.count()).toBe(11);
        });
 
        it('should update the ANY_MAP delivery service', function() {
diff --git a/traffic_portal/test/end_to_end/servers/servers-spec.js 
b/traffic_portal/test/end_to_end/servers/servers-spec.js
index fb3220b..1f5d240 100644
--- a/traffic_portal/test/end_to_end/servers/servers-spec.js
+++ b/traffic_portal/test/end_to_end/servers/servers-spec.js
@@ -41,11 +41,6 @@ describe('Traffic Portal Servers Test Suite', function() {
                
expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/servers");
        });
 
-       it('should verify CSV link exists ', function() {
-               console.log("Verify CSV button exists");
-               
expect(element(by.css('.dt-button.buttons-csv')).isPresent()).toBe(true);
-       });
-
        it('should open new Servers form page', function() {
                console.log('Clicking on Create new server ' + 
mockVals.hostName);
                
browser.driver.findElement(by.name('createServersButton')).click();
@@ -81,20 +76,14 @@ describe('Traffic Portal Servers Test Suite', function() {
                expect(first.isSelected()).toBe(true);
                first.click();
                expect(first.isSelected()).toBe(false);
-               let tableColumns = element.all(by.css('#serversTable 
tr:first-child td'));
-               expect(tableColumns.count()).toBe(11);
+               let tableColumns = element.all(by.css('.ag-header-cell'));
+               expect(tableColumns.count()).toBe(9);
        });
 
        it('should verify the new Server and then update Server', function() {
                console.log('Verifying new server added and updating ' + 
mockVals.hostName);
                browser.sleep(1000);
-               pageData.searchFilter.sendKeys(mockVals.hostName);
-               browser.sleep(250);
-               element.all(by.repeater('s in ::servers')).filter(function(row){
-                       return 
row.element(by.name('hostName')).getText().then(function(val){
-                               return val === mockVals.hostName;
-                       });
-               }).get(0).click();
+               element(by.cssContainingText('.ag-cell', 
mockVals.hostName)).click()
                browser.sleep(1000);
                pageData.domainName.clear();
                pageData.domainName.sendKeys('testupdated.com');

Reply via email to