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

shamrick 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 3bfe8e7e34 Add the ability to toggle sensitive data fields to common 
grids (#6990)
3bfe8e7e34 is described below

commit 3bfe8e7e34c47db06587a1716e1e1a99b1dcf64b
Author: ocket8888 <[email protected]>
AuthorDate: Thu Aug 25 09:19:23 2022 -0600

    Add the ability to toggle sensitive data fields to common grids (#6990)
    
    * Add the ability to toggle sensitive data fields to common grids
    
    (cherry picked from commit f4dc033306bc1f0450da770013773cef6a8fa197)
    
    * Fix tests checking for non-existent attribute
    
    * try getting a better error message
    
    * Refactor DS tests to get better error messages on every failure
    
    * Fix using non-existent expectation
    
    * Remove function that typescript says exists but protractor disagrees
    
    * Fix missing closing div tag
    
    * Fix broken CDNs DS table
    
    * Fix servers DSs table
    
    * Fix Service Category DS table
    
    (cherry picked from commit 72dd9edba6f2a63c1d8d2eeab68cf64ece8beff1)
    
    * Fix tenant DS table
    
    (cherry picked from commit a5d23cfecf40174ed29e20fe0b9fd60ccb8a6970)
    
    * Fix topologies DS table
    
    * Fix types DS table
    
    * Revert bad logical inversion
---
 .../FormServiceCategoryController.js               |    4 -
 .../serviceCategory/form.serviceCategory.tpl.html  |    2 +-
 .../modules/table/agGrid/CommonGridController.js   | 1103 +++++++++++---------
 .../src/common/modules/table/agGrid/grid.tpl.html  |  136 +--
 .../TableCDNDeliveryServicesController.js          |   15 +-
 .../table.cdnDeliveryServices.tpl.html             |  109 +-
 .../table.deliveryServices.tpl.html                |    1 +
 .../TableServerDeliveryServicesController.js       |  200 ++--
 .../table.serverDeliveryServices.tpl.html          |  116 +-
 ...bleServiceCategoryDeliveryServicesController.js |   13 +
 .../table.serviceCategoryDeliveryServices.tpl.html |  112 +-
 .../TableTenantDeliveryServicesController.js       |   13 +
 .../table.tenantDeliveryServices.tpl.html          |  110 +-
 .../TableTopologyDeliveryServicesController.js     |   21 +-
 .../table.topologyDeliveryServices.tpl.html        |  110 +-
 .../TableTypeDeliveryServicesController.js         |   26 +-
 .../table.typeDeliveryServices.tpl.html            |  110 +-
 traffic_portal/app/src/styles/main.scss            |    6 +
 .../test/integration/Data/deliveryservices.ts      |  156 ++-
 .../test/integration/PageObjects/BasePage.po.ts    |   12 +-
 .../PageObjects/DeliveryServicePage.po.ts          |  361 +++----
 .../integration/specs/DeliveryServices.spec.ts     |  123 +--
 22 files changed, 1217 insertions(+), 1642 deletions(-)

diff --git 
a/traffic_portal/app/src/common/modules/form/serviceCategory/FormServiceCategoryController.js
 
b/traffic_portal/app/src/common/modules/form/serviceCategory/FormServiceCategoryController.js
index 26014e8583..341ee1a447 100644
--- 
a/traffic_portal/app/src/common/modules/form/serviceCategory/FormServiceCategoryController.js
+++ 
b/traffic_portal/app/src/common/modules/form/serviceCategory/FormServiceCategoryController.js
@@ -29,10 +29,6 @@ var FormServiceCategoryController = 
function(serviceCategory, $scope, $location,
 
     $scope.hasPropertyError = formUtils.hasPropertyError;
 
-    $scope.viewDSs = function() {
-        $location.path('/service-categories/' + 
encodeURIComponent(serviceCategory.name) + '/delivery-services');
-    };
-
 };
 
 FormServiceCategoryController.$inject = ['serviceCategory', '$scope', 
'$location', 'formUtils', 'stringUtils', 'locationUtils'];
diff --git 
a/traffic_portal/app/src/common/modules/form/serviceCategory/form.serviceCategory.tpl.html
 
b/traffic_portal/app/src/common/modules/form/serviceCategory/form.serviceCategory.tpl.html
index 44274416fe..0bb4fa36cf 100644
--- 
a/traffic_portal/app/src/common/modules/form/serviceCategory/form.serviceCategory.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/form/serviceCategory/form.serviceCategory.tpl.html
@@ -24,7 +24,7 @@ under the License.
             <li class="active">{{serviceCategoryName}}</li>
         </ol>
         <div class="pull-right" role="group" ng-show="!settings.isNew">
-            <button type=button class="btn btn-primary" title="View Delivery 
Services" ng-click="viewDSs()">View Delivery Services</button>
+            <a class="btn btn-primary" title="View Delivery Services" 
ng-href="#!/service-categories/{{serviceCategory.name}}/delivery-services">View 
Delivery Services</a>
         </div>
         <div class="clearfix"></div>
     </div>
diff --git 
a/traffic_portal/app/src/common/modules/table/agGrid/CommonGridController.js 
b/traffic_portal/app/src/common/modules/table/agGrid/CommonGridController.js
index c8571f4f7d..4bedb383de 100644
--- a/traffic_portal/app/src/common/modules/table/agGrid/CommonGridController.js
+++ b/traffic_portal/app/src/common/modules/table/agGrid/CommonGridController.js
@@ -29,529 +29,598 @@
  * @param {GridApi} api
  */
 function setUpQueryParamFilter(params, columns, api) {
-    for (const col of columns) {
-        if (!Object.prototype.hasOwnProperty.call(col, "field")) {
-            continue;
-        }
-        const filter = api.getFilterInstance(col.field);
-        if (!filter) {
-            continue;
-        }
-        const values = params.getAll(col.field);
-        if (values.length < 1) {
-            continue;
-        }
-
-        /** @type {"string" | "number" | "date"} */
-        let colType;
-        if (!Object.prototype.hasOwnProperty.call(col, "filter")) {
-            colType = "string";
-        } else if (typeof(col.filter) !== "string") {
-            continue;
-        } else {
-            let bail = false;
-            switch(col.filter) {
-                case "agTextColumnFilter":
-                    colType = "string";
-                    break;
-                case "agNumberColumnFilter":
-                    colType = "number";
-                    break;
-                case "agDateColumnFilter":
-                    colType = "date";
-                    break;
-                default:
-                    bail = true;
-                    break;
-            }
-            if (bail) {
-                continue;
-            }
-        }
-
-        let filterModel;
-        switch(colType) {
-            case "string":
-                if (values.length === 1) {
-                    filterModel = {
-                        filter: values[0],
-                        type: "equals"
-                    }
-                } else {
-                    filterModel = {
-                        operator: "OR",
-                        condition1: {
-                            filter: values[0],
-                            type: "equals"
-                        },
-                        condition2: {
-                            filter: values[1],
-                            type: "equals"
-                        }
-                    }
-                }
-                break;
-            case "number":
-                if (values.length === 1) {
-                    filterModel = {
-                        filter: parseInt(values[0], 10),
-                        type: "equals"
-                    }
-                    if (isNaN(filterModel.filter)) {
-                        continue;
-                    }
-                } else {
-                    filterModel = {
-                        operator: "OR",
-                        condition1: {
-                            filter: parseInt(values[0], 10),
-                            type: "equals"
-                        },
-                        condition2: {
-                            filter: parseInt(values[1], 10),
-                            type: "equals"
-                        }
-                    }
-                    if (isNaN(filterModel.condition1.filter) || 
isNaN(filterModel.condition2.filter)) {
-                        continue;
-                    }
-                }
-                break;
-            case "date":
-                const date = new Date(values[0]);
-                if (isNaN(date)) {
-                    continue;
-                }
-                const pad = num => String(num).padStart(2,"0");
-                filterModel = {
-                    dateFrom: 
`${date.getUTCFullYear()}-${pad(date.getUTCMonth()+1)}-${pad(date.getUTCDate())}
 
${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}`,
-                    type: "equals"
-                }
-                break;
-        }
-        filter.setModel(filterModel);
-        filter.applyModel();
-    }
+       for (const col of columns) {
+               if (!Object.prototype.hasOwnProperty.call(col, "field")) {
+                       continue;
+               }
+               const filter = api.getFilterInstance(col.field);
+               if (!filter) {
+                       continue;
+               }
+               const values = params.getAll(col.field);
+               if (values.length < 1) {
+                       continue;
+               }
+
+               /** @type {"string" | "number" | "date"} */
+               let colType;
+               if (!Object.prototype.hasOwnProperty.call(col, "filter")) {
+                       colType = "string";
+               } else if (typeof(col.filter) !== "string") {
+                       continue;
+               } else {
+                       let bail = false;
+                       switch(col.filter) {
+                               case "agTextColumnFilter":
+                                       colType = "string";
+                                       break;
+                               case "agNumberColumnFilter":
+                                       colType = "number";
+                                       break;
+                               case "agDateColumnFilter":
+                                       colType = "date";
+                                       break;
+                               default:
+                                       bail = true;
+                                       break;
+                       }
+                       if (bail) {
+                               continue;
+                       }
+               }
+
+               let filterModel;
+               switch(colType) {
+                       case "string":
+                               if (values.length === 1) {
+                                       filterModel = {
+                                               filter: values[0],
+                                               type: "equals"
+                                       }
+                               } else {
+                                       filterModel = {
+                                               operator: "OR",
+                                               condition1: {
+                                                       filter: values[0],
+                                                       type: "equals"
+                                               },
+                                               condition2: {
+                                                       filter: values[1],
+                                                       type: "equals"
+                                               }
+                                       }
+                               }
+                               break;
+                       case "number":
+                               if (values.length === 1) {
+                                       filterModel = {
+                                               filter: parseInt(values[0], 10),
+                                               type: "equals"
+                                       }
+                                       if (isNaN(filterModel.filter)) {
+                                               continue;
+                                       }
+                               } else {
+                                       filterModel = {
+                                               operator: "OR",
+                                               condition1: {
+                                                       filter: 
parseInt(values[0], 10),
+                                                       type: "equals"
+                                               },
+                                               condition2: {
+                                                       filter: 
parseInt(values[1], 10),
+                                                       type: "equals"
+                                               }
+                                       }
+                                       if 
(isNaN(filterModel.condition1.filter) || isNaN(filterModel.condition2.filter)) {
+                                               continue;
+                                       }
+                               }
+                               break;
+                       case "date":
+                               const date = new Date(values[0]);
+                               if (Number.isNaN(date.getTime())) {
+                                       continue;
+                               }
+                               const pad = num => String(num).padStart(2,"0");
+                               filterModel = {
+                                       dateFrom: 
`${date.getUTCFullYear()}-${pad(date.getUTCMonth()+1)}-${pad(date.getUTCDate())}
 
${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}`,
+                                       type: "equals"
+                               }
+                               break;
+               }
+               filter.setModel(filterModel);
+               filter.applyModel();
+       }
 }
 
 let CommonGridController = function ($scope, $document, $state, userModel, 
dateUtils) {
-    this.entry = null;
-    this.quickSearch = "";
-    this.pageSize = 100;
-    this.showMenu = false;
-    this.menuStyle = {
-        left: 0,
-        top: 0
-    };
-    this.mouseDownSelectionText = "";
-
-    // Bound Variables
-    /** @type string */
-    this.tableName = "";
-    /** @type CGC.GridSettings */
-    this.options = {};
-    /** @type any[] */
-    this.columns = [];
-    /** @type any[] */
-    this.data = [];
-    /** @type any[] */
-    this.selectedData = [];
-    /** @type any */
-    this.defaultData = {};
-    /** @type CGC.DropDownOption[] */
-    this.dropDownOptions = [];
-    /** @type CGC.ContextMenuOption[] */
-    this.contextMenuOptions = [];
-    /** @type CGC.TitleButton */
-    this.titleButton = {};
-    /** @type CGC.TitleBreadCrumbs */
-    this.breadCrumbs = [];
-
-    function HTTPSCellRenderer() {}
-    HTTPSCellRenderer.prototype.init = function(params) {
-        this.eGui = document.createElement("a");
-        this.eGui.href = "https://"; + params.value;
-        this.eGui.setAttribute("class", "link");
-        this.eGui.setAttribute("target", "_blank");
-        this.eGui.textContent = params.value;
-    };
-    HTTPSCellRenderer.prototype.getGui = function() {return this.eGui;};
-
-    // 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("class", "link");
-        this.eGui.textContent = params.value;
-    };
-    SSHCellRenderer.prototype.getGui = function() {return this.eGui;};
-
-    function CheckCellRenderer() {}
-    CheckCellRenderer.prototype.init = function(params) {
-        this.eGui = document.createElement("i");
-        if (params.value === null || params.value === undefined) {
-            return;
-        }
-
-        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-check");
-        } else {
-            this.eGui.classList.add("fa-times");
-        }
-    };
-    CheckCellRenderer.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;};
-
-    function defaultTooltip(params) {
-        return params.value;
-    }
-
-    function dateCellFormatterRelative(params) {
-        return params.value ? dateUtils.getRelativeTime(params.value) : 
params.value;
-    }
-
-    function dateCellFormatterUTC(params) {
-        return params.value ? params.value.toUTCString() : params.value;
-    }
-
-    this.hasContextItems = function() {
-        return this.contextMenuOptions.length > 0;
-    };
-
-    this.$onInit = function() {
-        let tableName = this.tableName;
-        let self = this;
-
-        if (self.defaultData !== undefined) {
-            self.entry = self.defaultData;
-        }
-
-        for(let i = 0; i < self.columns.length; ++i) {
-            if (self.columns[i].filter === "agDateColumnFilter") {
-                if (self.columns[i].relative !== undefined && 
self.columns[i].relative === true) {
-                    self.columns[i].tooltipValueGetter = 
dateCellFormatterRelative;
-                    self.columns[i].valueFormatter = dateCellFormatterRelative;
-                }
-                else {
-                    self.columns[i].tooltipValueGetter = dateCellFormatterUTC;
-                    self.columns[i].valueFormatter = dateCellFormatterUTC;
-                }
-            }
-        }
-
-        // clicks outside the context menu will hide it
-        $document.bind("click", function(e) {
-            self.showMenu = false;
-            e.stopPropagation();
-            $scope.$apply();
-        });
-
-        this.gridOptions = {
-            components: {
-                httpsCellRenderer: HTTPSCellRenderer,
-                sshCellRenderer: SSHCellRenderer,
-                updateCellRenderer: UpdateCellRenderer,
-                checkCellRenderer: CheckCellRenderer,
-            },
-            columnDefs: self.columns,
-            enableCellTextSelection: true,
-            suppressMenuHide: true,
-            multiSortKey: 'ctrl',
-            alwaysShowVerticalScroll: true,
-            defaultColDef: {
-                filter: true,
-                sortable: true,
-                resizable: true,
-                tooltipValueGetter: defaultTooltip
-            },
-            rowClassRules: self.options.rowClassRules,
-            rowData: self.data,
-            pagination: true,
-            paginationPageSize: self.pageSize,
-            rowBuffer: 0,
-            onColumnResized: function() {
-                localStorage.setItem(tableName + "_table_columns", 
JSON.stringify(self.gridOptions.columnApi.getColumnState()));
-            },
-            colResizeDefault: "shift",
-            tooltipShowDelay: 500,
-            allowContextMenuWithControlKey: true,
-            preventDefaultOnContextMenu: self.hasContextItems(),
-            onCellMouseDown: function() {
-                self.mouseDownSelectionText = window.getSelection().toString();
-            },
-            onCellContextMenu: function(params) {
-                if (!self.hasContextItems()){
-                    return;
-                }
-                self.showMenu = true;
-                self.menuStyle.left = String(params.event.clientX) + "px";
-                self.menuStyle.top = String(params.event.clientY) + "px";
-                self.menuStyle.bottom = "unset";
-                self.menuStyle.right = "unset";
-                $scope.$apply();
-                const boundingRect = 
document.getElementById("context-menu").getBoundingClientRect();
-
-                if (boundingRect.bottom > window.innerHeight){
-                    self.menuStyle.bottom = String(window.innerHeight - 
params.event.clientY) + "px";
-                    self.menuStyle.top = "unset";
-                }
-                if (boundingRect.right > window.innerWidth) {
-                    self.menuStyle.right = String(window.innerWidth - 
params.event.clientX) + "px";
-                    self.menuStyle.left = "unset";
-                }
-                self.entry = params.data;
-                $scope.$apply();
-            },
-            onColumnVisible: function(params) {
-                if (params.visible){
-                    return;
-                }
-                for (let column of params.columns) {
-                    if (column.filterActive) {
-                        const filterModel = 
self.gridOptions.api.getFilterModel();
-                        if (column.colId in filterModel) {
-                            delete filterModel[column.colId];
-                            self.gridOptions.api.setFilterModel(filterModel);
-                        }
-                    }
-                }
-            },
-            onRowSelected: function() {
-                self.selectedData = self.gridOptions.api.getSelectedRows();
-                $scope.$apply();
-            },
-            onSelectionChanged: function() {
-                self.selectedData = self.gridOptions.api.getSelectedRows();
-                $scope.$apply();
-            },
-            onRowClicked: function(params) {
-                if (params.event.target instanceof HTMLAnchorElement) {
-                    return;
-                }
-                const selection = window.getSelection().toString();
-                if(self.options.onRowClick !== undefined && (selection === "" 
|| selection === $scope.mouseDownSelectionText)) {
-                    self.options.onRowClick(params);
-                    $scope.$apply();
-                }
-                $scope.mouseDownSelectionText = "";
-            },
-            onFirstDataRendered: function() {
-                if(self.options.selectRows) {
-                    self.gridOptions.rowSelection = self.options.selectRows ? 
"multiple" : "";
-                    self.gridOptions.rowMultiSelectWithClick = 
self.options.selectRows;
-                    self.gridOptions.api.forEachNode(node => {
-                        if (node.data[self.options.selectionProperty] === 
true) {
-                            node.setSelected(true, false);
-                        }
-                    });
-                }
-                try {
-                    const filterState = 
JSON.parse(localStorage.getItem(tableName + "_table_filters")) || {};
-                    self.gridOptions.api.setFilterModel(filterState);
-                } catch (e) {
-                    console.error("Failure to load stored filter state:", e);
-                }
-                // Set up filters from query string paramters.
-                const params = new 
URLSearchParams(globalThis.location.hash.split("?").slice(1).join("?"));
-                setUpQueryParamFilter(params, self.columns, 
self.gridOptions.api);
-                self.gridOptions.api.onFilterChanged();
-
-                self.gridOptions.api.addEventListener("filterChanged", 
function() {
-                    localStorage.setItem(tableName + "_table_filters", 
JSON.stringify(self.gridOptions.api.getFilterModel()));
-                });
-            },
-            onGridReady: function() {
-                try {
-                    // need to create the show/hide column checkboxes and bind 
to the current visibility
-                    const colstates = 
JSON.parse(localStorage.getItem(tableName + "_table_columns"));
-                    if (colstates) {
-                        if 
(!self.gridOptions.columnApi.setColumnState(colstates)) {
-                            console.error("Failed to load stored column state: 
one or more columns not found");
-                        }
-                    } else {
-                        self.gridOptions.api.sizeColumnsToFit();
-                    }
-                } catch (e) {
-                    console.error("Failure to retrieve required column info 
from localStorage (key=" + tableName + "_table_columns):", e);
-                }
-
-                try {
-                    const sortState = 
JSON.parse(localStorage.getItem(tableName + "_table_sort"));
-                    self.gridOptions.api.setSortModel(sortState);
-                } catch (e) {
-                    console.error("Failure to load stored sort state:", e);
-                }
-
-                try {
-                    self.quickSearch = localStorage.getItem(tableName + 
"_quick_search");
-                    self.gridOptions.api.setQuickFilter(self.quickSearch);
-                } catch (e) {
-                    console.error("Failure to load stored quick search:", e);
-                }
-
-                try {
-                    const ps = localStorage.getItem(tableName + "_page_size");
-                    if (ps && ps > 0) {
-                        self.pageSize = Number(ps);
-                        
self.gridOptions.api.paginationSetPageSize(self.pageSize);
-                    }
-                } catch (e) {
-                    console.error("Failure to load stored page size:", e);
-                }
-
-                try {
-                    const page = parseInt(localStorage.getItem(tableName + 
"_table_page"));
-                    if (page !== undefined && page > 0 && page <= 
$scope.gridOptions.api.paginationGetTotalPages()-1) {
-                        $scope.gridOptions.api.paginationGoToPage(page);
-                    }
-                } catch (e) {
-                    console.error("Failed to load stored page number:", e);
-                }
-
-                self.gridOptions.api.addEventListener("sortChanged", 
function() {
-                    localStorage.setItem(tableName + "_table_sort", 
JSON.stringify(self.gridOptions.api.getSortModel()));
-                });
-
-                self.gridOptions.api.addEventListener("columnMoved", 
function() {
-                    localStorage.setItem(tableName + "_table_columns", 
JSON.stringify(self.gridOptions.columnApi.getColumnState()));
-                });
-
-                self.gridOptions.api.addEventListener("columnVisible", 
function() {
-                    self.gridOptions.api.sizeColumnsToFit();
-                    try {
-                        const colStates = 
self.gridOptions.columnApi.getColumnState();
-                        localStorage.setItem(tableName + "_table_columns", 
JSON.stringify(colStates));
-                    } catch (e) {
-                        console.error("Failed to store column defs to local 
storage:", e);
-                    }
-                });
-            }
-        };
-
-    };
-
-    this.exportCSV = function() {
-        const params = {
-            allColumns: true,
-            fileName: this.tableName + ".csv",
-        };
-        this.gridOptions.api.exportDataAsCsv(params);
-    };
-
-    this.toggleVisibility = function(col) {
-        const visible = this.gridOptions.columnApi.getColumn(col).isVisible();
-        this.gridOptions.columnApi.setColumnVisible(col, !visible);
-    };
-
-    this.onQuickSearchChanged = function() {
-        this.gridOptions.api.setQuickFilter(this.quickSearch);
-        localStorage.setItem(this.tableName + "_quick_search", 
this.quickSearch);
-    };
-
-    this.onPageSizeChanged = function() {
-        const value = Number(this.pageSize);
-        this.gridOptions.api.paginationSetPageSize(value);
-        localStorage.setItem(this.tableName + "_page_size", value);
-    };
-
-    this.clearTableFilters = function() {
-        // clear the quick search
-        this.quickSearch = '';
-        this.onQuickSearchChanged();
-        // clear any column filters
-        this.gridOptions.api.setFilterModel(null);
-    };
-
-    this.contextMenuClick = function(menu, $event) {
-        $event.stopPropagation();
-        menu.onClick(this.entry);
-    };
-
-    this.getHref = function(menu) {
-        if (menu.href !== undefined){
-            return menu.href;
-        }
-        return menu.getHref(this.entry);
-    };
-
-    this.contextIsDisabled = function(menu) {
-        if (menu.isDisabled !== undefined) {
-            return menu.isDisabled(this.entry);
-        }
-        return false;
-    };
-
-    this.bcGetText = function (bc) {
-        if(bc.text !== undefined){
-            return bc.text;
-        }
-        return bc.getText();
-    };
-
-    this.bcHasHref = function(bc) {
-        return bc.href !== undefined || bc.getHref !== undefined;
-    };
-
-    this.bcGetHref = function(bc) {
-        if(bc.href !== undefined) {
-            return bc.href;
-        }
-        return bc.getHref();
-    };
-
-    this.getText = function (menu) {
-        if (menu.text !== undefined){
-            return menu.text;
-        }
-        return menu.getText(this.entry);
-    };
-
-    this.isShown = function (menu) {
-        if (menu.shown === undefined){
-            return true;
-        }
-        return menu.shown(this.entry);
-    };
-
-    $scope.refresh = function() {
-        $state.reload(); // reloads all the resolves for the view
-    };
+       this.entry = null;
+       this.quickSearch = "";
+       this.pageSize = 100;
+       this.showMenu = false;
+       /**
+        * @type {{
+        *      bottom?: string | 0;
+        *      left: string | 0;
+        *      right?: string | 0;
+        *      top: string | 0;
+        * }}
+        */
+       this.menuStyle = {
+               left: 0,
+               top: 0
+       };
+       this.mouseDownSelectionText = "";
+
+       // Bound Variables
+       /** @type string */
+       this.tableTitle = "";
+       /** @type string */
+       this.tableName = "";
+       /** @type CGC.GridSettings */
+       this.options = {};
+       /** @type any */
+       this.gridOptions = {};
+       /** @type any[] */
+       this.columns = [];
+       /** @type string[] */
+       this.sensitiveColumns = [];
+       /** @type any[] */
+       this.data = [];
+       /** @type any[] */
+       this.selectedData = [];
+       /** @type any */
+       this.defaultData = {};
+       /** @type CGC.DropDownOption[] */
+       this.dropDownOptions = [];
+       /** @type CGC.ContextMenuOption[] */
+       this.contextMenuOptions = [];
+       /** @type CGC.TitleButton */
+       this.titleButton = {};
+       /** @type CGC.TitleBreadCrumbs */
+       this.breadCrumbs = [];
+
+       function HTTPSCellRenderer() {}
+       HTTPSCellRenderer.prototype.init = function(params) {
+               this.eGui = document.createElement("a");
+               this.eGui.href = "https://"; + params.value;
+               this.eGui.setAttribute("class", "link");
+               this.eGui.setAttribute("target", "_blank");
+               this.eGui.textContent = params.value;
+       };
+       HTTPSCellRenderer.prototype.getGui = function() {return this.eGui;};
+
+       // 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("class", "link");
+               this.eGui.textContent = params.value;
+       };
+       SSHCellRenderer.prototype.getGui = function() {return this.eGui;};
+
+       function CheckCellRenderer() {}
+       CheckCellRenderer.prototype.init = function(params) {
+               this.eGui = document.createElement("i");
+               if (params.value === null || params.value === undefined) {
+                       return;
+               }
+
+               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-check");
+               } else {
+                       this.eGui.classList.add("fa-times");
+               }
+       };
+       CheckCellRenderer.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;};
+
+       function defaultTooltip(params) {
+               return params.value;
+       }
+
+       function dateCellFormatterRelative(params) {
+               return params.value ? dateUtils.getRelativeTime(params.value) : 
params.value;
+       }
+
+       function dateCellFormatterUTC(params) {
+               return params.value ? params.value.toUTCString() : params.value;
+       }
+
+       this.hasContextItems = function() {
+               return this.contextMenuOptions.length > 0;
+       };
+
+       this.hasSensitiveColumns = function() {
+               return this.sensitiveColumns.length > 0;
+       }
+
+       /**
+        * @param {string} colID
+        */
+       this.isSensitive = function(colID) {
+               return this.sensitiveColumns.includes(colID);
+       }
+
+       this.sensitiveColumnsShown = false;
+
+       this.toggleSensitiveFields = function() {
+               if (this.sensitiveColumnsShown) {
+                       return;
+               }
+               for (const col of this.gridOptions.columnApi.getAllColumns()) {
+                       const id = col.getColId();
+                       if (this.isSensitive(id)) {
+                               this.gridOptions.columnApi.setColumnVisible(id, 
false);
+                       }
+               }
+       };
+
+       this.getColumns = () => {
+               /** @type {{colId: string}[]} */
+               const cols = this.gridOptions.columnApi.getAllColumns();
+               if (!this.hasSensitiveColumns || this.sensitiveColumnsShown) {
+                       return cols;
+               }
+               return cols.filter(c => !this.isSensitive(c.colId));
+       }
+
+       this.$onInit = () => {
+               const tableName = this.tableName;
+
+               if (this.defaultData !== undefined) {
+                       this.entry = this.defaultData;
+               }
+
+               for(let i = 0; i < this.columns.length; ++i) {
+                       if (this.columns[i].filter === "agDateColumnFilter") {
+                               if (this.columns[i].relative) {
+                                       this.columns[i].tooltipValueGetter = 
dateCellFormatterRelative;
+                                       this.columns[i].valueFormatter = 
dateCellFormatterRelative;
+                               }
+                               else {
+                                       this.columns[i].tooltipValueGetter = 
dateCellFormatterUTC;
+                                       this.columns[i].valueFormatter = 
dateCellFormatterUTC;
+                               }
+                       }
+               }
+
+               // clicks outside the context menu will hide it
+               $document.bind("click", e => {
+                       this.showMenu = false;
+                       e.stopPropagation();
+                       $scope.$apply();
+               });
+
+               this.gridOptions = {
+                       components: {
+                               httpsCellRenderer: HTTPSCellRenderer,
+                               sshCellRenderer: SSHCellRenderer,
+                               updateCellRenderer: UpdateCellRenderer,
+                               checkCellRenderer: CheckCellRenderer,
+                       },
+                       columnDefs: this.columns,
+                       enableCellTextSelection: true,
+                       suppressMenuHide: true,
+                       multiSortKey: 'ctrl',
+                       alwaysShowVerticalScroll: true,
+                       defaultColDef: {
+                               filter: true,
+                               sortable: true,
+                               resizable: true,
+                               tooltipValueGetter: defaultTooltip
+                       },
+                       rowClassRules: this.options.rowClassRules,
+                       rowData: this.data,
+                       pagination: true,
+                       paginationPageSize: this.pageSize,
+                       rowBuffer: 0,
+                       onColumnResized: () => {
+                               /** @type {{colId: string; hide?: boolean | 
null}[]} */
+                               const states = 
this.gridOptions.columnApi.getColumnState();
+                               for (const state of states) {
+                                       state.hide = state.hide || 
this.isSensitive(state.colId);
+                               }
+                               localStorage.setItem(tableName + 
"_table_columns", JSON.stringify(states));
+                       },
+                       colResizeDefault: "shift",
+                       tooltipShowDelay: 500,
+                       allowContextMenuWithControlKey: true,
+                       preventDefaultOnContextMenu: this.hasContextItems(),
+                       onCellMouseDown: () => {
+                               const selection = window.getSelection();
+                               if (!selection) {
+                                       this.mouseDownSelectionText = "";
+                               } else {
+                                       this.mouseDownSelectionText = 
selection.toString();
+                               }
+                       },
+                       onCellContextMenu: params => {
+                               if (!this.hasContextItems()){
+                                       return;
+                               }
+                               this.showMenu = true;
+                               this.menuStyle.left = 
String(params.event.clientX) + "px";
+                               this.menuStyle.top = 
String(params.event.clientY) + "px";
+                               this.menuStyle.bottom = "unset";
+                               this.menuStyle.right = "unset";
+                               $scope.$apply();
+                               const boundingRect = 
document.getElementById("context-menu")?.getBoundingClientRect();
+                               if (!boundingRect) {
+                                       throw new Error("no bounding rectangle 
for context-menu; element possibly missing");
+                               }
+
+                               if (boundingRect.bottom > window.innerHeight){
+                                       this.menuStyle.bottom = 
String(window.innerHeight - params.event.clientY) + "px";
+                                       this.menuStyle.top = "unset";
+                               }
+                               if (boundingRect.right > window.innerWidth) {
+                                       this.menuStyle.right = 
String(window.innerWidth - params.event.clientX) + "px";
+                                       this.menuStyle.left = "unset";
+                               }
+                               this.entry = params.data;
+                               $scope.$apply();
+                       },
+                       onColumnVisible: params => {
+                               if (params.visible){
+                                       return;
+                               }
+                               for (let column of params.columns) {
+                                       if (column.filterActive) {
+                                               const filterModel = 
this.gridOptions.api.getFilterModel();
+                                               if (column.colId in 
filterModel) {
+                                                       delete 
filterModel[column.colId];
+                                                       
this.gridOptions.api.setFilterModel(filterModel);
+                                               }
+                                       }
+                               }
+                       },
+                       onRowSelected: () => {
+                               this.selectedData = 
this.gridOptions.api.getSelectedRows();
+                               $scope.$apply();
+                       },
+                       onSelectionChanged: () => {
+                               this.selectedData = 
this.gridOptions.api.getSelectedRows();
+                               $scope.$apply();
+                       },
+                       onRowClicked: params => {
+                               if (params.event.target instanceof 
HTMLAnchorElement) {
+                                       return;
+                               }
+                               const selection = window.getSelection();
+                               if (this.options.onRowClick !== undefined) {
+                                       if (!selection || selection.toString() 
=== "" || selection === $scope.mouseDownSelectionText) {
+                                               this.options.onRowClick(params);
+                                               $scope.$apply();
+                                       }
+                               }
+                               $scope.mouseDownSelectionText = "";
+                       },
+                       onFirstDataRendered: () => {
+                               if(this.options.selectRows) {
+                                       this.gridOptions.rowSelection = 
this.options.selectRows ? "multiple" : "";
+                                       
this.gridOptions.rowMultiSelectWithClick = this.options.selectRows;
+                                       this.gridOptions.api.forEachNode(node 
=> {
+                                               if 
(node.data[this.options.selectionProperty] === true) {
+                                                       node.setSelected(true, 
false);
+                                               }
+                                       });
+                               }
+                               try {
+                                       const filterState = 
JSON.parse(localStorage.getItem(tableName + "_table_filters") ?? "{}") || {};
+                                       
this.gridOptions.api.setFilterModel(filterState);
+                               } catch (e) {
+                                       console.error("Failure to load stored 
filter state:", e);
+                               }
+                               // Set up filters from query string paramters.
+                               const params = new 
URLSearchParams(globalThis.location.hash.split("?").slice(1).join("?"));
+                               setUpQueryParamFilter(params, this.columns, 
this.gridOptions.api);
+                               this.gridOptions.api.onFilterChanged();
+
+                               
this.gridOptions.api.addEventListener("filterChanged", () => {
+                                       localStorage.setItem(tableName + 
"_table_filters", JSON.stringify(this.gridOptions.api.getFilterModel()));
+                               });
+                       },
+                       onGridReady: () => {
+                               try {
+                                       // need to create the show/hide column 
checkboxes and bind to the current visibility
+                                       const colstates = 
JSON.parse(localStorage.getItem(tableName + "_table_columns") ?? "null");
+                                       if (colstates) {
+                                               if 
(!this.gridOptions.columnApi.setColumnState(colstates)) {
+                                                       console.error("Failed 
to load stored column state: one or more columns not found");
+                                               }
+                                       } else {
+                                               
this.gridOptions.api.sizeColumnsToFit();
+                                       }
+                               } catch (e) {
+                                       console.error("Failure to retrieve 
required column info from localStorage (key=" + tableName + "_table_columns):", 
e);
+                               }
+
+                               try {
+                                       const sortState = 
JSON.parse(localStorage.getItem(tableName + "_table_sort") ?? "{}");
+                                       
this.gridOptions.api.setSortModel(sortState);
+                               } catch (e) {
+                                       console.error("Failure to load stored 
sort state:", e);
+                               }
+
+                               try {
+                                       this.quickSearch = 
localStorage.getItem(tableName + "_quick_search") ?? "";
+                                       
this.gridOptions.api.setQuickFilter(this.quickSearch);
+                               } catch (e) {
+                                       console.error("Failure to load stored 
quick search:", e);
+                               }
+
+                               try {
+                                       const ps = 
Number(localStorage.getItem(tableName + "_page_size"));
+                                       if (ps > 0) {
+                                               this.pageSize = Number(ps);
+                                               
this.gridOptions.api.paginationSetPageSize(this.pageSize);
+                                       }
+                               } catch (e) {
+                                       console.error("Failure to load stored 
page size:", e);
+                               }
+
+                               try {
+                                       const page = 
parseInt(localStorage.getItem(tableName + "_table_page") ?? "0", 10);
+                                       if (page > 0 && page <= 
$scope.gridOptions.api.paginationGetTotalPages()-1) {
+                                               
$scope.gridOptions.api.paginationGoToPage(page);
+                                       }
+                               } catch (e) {
+                                       console.error("Failed to load stored 
page number:", e);
+                               }
+
+                               
this.gridOptions.api.addEventListener("sortChanged", () => {
+                                       localStorage.setItem(tableName + 
"_table_sort", JSON.stringify(this.gridOptions.api.getSortModel()));
+                               });
+
+                               
this.gridOptions.api.addEventListener("columnMoved", () => {
+                                       /** @type {{colId: string; hide?: 
boolean | null}[]} */
+                                       const states = 
this.gridOptions.columnApi.getColumnState();
+                                       for (const state of states) {
+                                               state.hide = state.hide || 
this.isSensitive(state.colId);
+                                       }
+
+                                       localStorage.setItem(tableName + 
"_table_columns", JSON.stringify(this.gridOptions.columnApi.getColumnState()));
+                               });
+
+                               
this.gridOptions.api.addEventListener("columnVisible", () => {
+                                       this.gridOptions.api.sizeColumnsToFit();
+                                       try {
+                                               const colStates = 
this.gridOptions.columnApi.getColumnState();
+                                               localStorage.setItem(tableName 
+ "_table_columns", JSON.stringify(colStates));
+                                       } catch (e) {
+                                               console.error("Failed to store 
column defs to local storage:", e);
+                                       }
+                               });
+                       }
+               };
+
+       };
+
+       this.exportCSV = function() {
+               const params = {
+                       allColumns: true,
+                       fileName: this.tableName + ".csv",
+               };
+               this.gridOptions.api.exportDataAsCsv(params);
+       };
+
+       this.toggleVisibility = function(col) {
+               const visible = 
this.gridOptions.columnApi.getColumn(col).isVisible();
+               this.gridOptions.columnApi.setColumnVisible(col, !visible);
+       };
+
+       this.onQuickSearchChanged = function() {
+               this.gridOptions.api.setQuickFilter(this.quickSearch);
+               localStorage.setItem(this.tableName + "_quick_search", 
this.quickSearch);
+       };
+
+       this.onPageSizeChanged = function() {
+               const value = Number(this.pageSize);
+               this.gridOptions.api.paginationSetPageSize(value);
+               localStorage.setItem(this.tableName + "_page_size", 
value.toString());
+       };
+
+       this.clearTableFilters = () => {
+               // clear the quick search
+               this.quickSearch = '';
+               this.onQuickSearchChanged();
+               // clear any column filters
+               this.gridOptions.api.setFilterModel(null);
+       };
+
+       this.contextMenuClick = function(menu, $event) {
+               $event.stopPropagation();
+               menu.onClick(this.entry);
+       };
+
+       this.getHref = function(menu) {
+               if (menu.href !== undefined){
+                       return menu.href;
+               }
+               return menu.getHref(this.entry);
+       };
+
+       this.contextIsDisabled = function(menu) {
+               if (menu.isDisabled !== undefined) {
+                       return menu.isDisabled(this.entry);
+               }
+               return false;
+       };
+
+       this.bcGetText = function (bc) {
+               if(bc.text !== undefined){
+                       return bc.text;
+               }
+               return bc.getText();
+       };
+
+       this.bcHasHref = function(bc) {
+               return bc.href !== undefined || bc.getHref !== undefined;
+       };
+
+       this.bcGetHref = function(bc) {
+               if(bc.href !== undefined) {
+                       return bc.href;
+               }
+               return bc.getHref();
+       };
+
+       this.getText = function (menu) {
+               if (menu.text !== undefined){
+                       return menu.text;
+               }
+               return menu.getText(this.entry);
+       };
+
+       this.isShown = function (menu) {
+               if (menu.shown === undefined){
+                       return true;
+               }
+               return menu.shown(this.entry);
+       };
+
+       $scope.refresh = function() {
+               $state.reload(); // reloads all the resolves for the view
+       };
 };
 
 angular.module("trafficPortal.table").component("commonGridController", {
-    templateUrl: "common/modules/table/agGrid/grid.tpl.html",
-    controller: CommonGridController,
-    bindings: {
-        tableTitle: "@",
-        tableName: "@",
-        options: "<",
-        columns: "<",
-        data: "<",
-        selectedData: "=?",
-        dropDownOptions: "<?",
-        contextMenuOptions: "<?",
-        defaultData: "<?",
-        titleButton: "<?",
-        breadCrumbs: "<?"
-    }
+       templateUrl: "common/modules/table/agGrid/grid.tpl.html",
+       controller: CommonGridController,
+       bindings: {
+               tableTitle: "@",
+               tableName: "@",
+               options: "<",
+               columns: "<",
+               data: "<",
+               selectedData: "=?",
+               dropDownOptions: "<?",
+               contextMenuOptions: "<?",
+               defaultData: "<?",
+               titleButton: "<?",
+               breadCrumbs: "<?",
+               sensitiveColumns: "<?"
+       }
 });
 
 CommonGridController.$inject = ["$scope", "$document", "$state", "userModel", 
"dateUtils"];
diff --git a/traffic_portal/app/src/common/modules/table/agGrid/grid.tpl.html 
b/traffic_portal/app/src/common/modules/table/agGrid/grid.tpl.html
index 9b8bda766b..68866af084 100644
--- a/traffic_portal/app/src/common/modules/table/agGrid/grid.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/agGrid/grid.tpl.html
@@ -18,75 +18,79 @@ under the License.
 -->
 
 <div class="x_title grid-comp">
-    <div class="pull-left">
-        <ol class="breadcrumb pull-left" ng-if="$ctrl.breadCrumbs !== 
undefined">
-            <li ng-repeat="bc in $ctrl.breadCrumbs track by $index" 
ng-class="{'active': ($index+1 === $ctrl.breadCrumbs.length)}">
-                <a ng-if="$ctrl.bcHasHref(bc)" ng-href="{{ $ctrl.bcGetHref(bc) 
}}">{{ $ctrl.bcGetText(bc) }}</a>
-                <span class="bc" ng-if="!$ctrl.bcHasHref(bc)">{{  
$ctrl.bcGetText(bc) }}</span>
-            </li>
-        </ol>
-        <ol class="breadcrumb pull-left" ng-if="$ctrl.breadCrumbs === 
undefined || $ctrl.breadCrumbs.length < 1">
-            <li class="active" ng-if="$ctrl.tableTitle">
-                {{ $ctrl.tableTitle }}
-            </li>
-        </ol>
-        <button ng-if="$ctrl.titleButton !== undefined" type="button" 
class="btn btn-link"
-                ng-click="$ctrl.titleButton.onClick()">{{ 
$ctrl.titleButton.getText() }}</button>
-        <span ng-if="$ctrl.options.selectRows">{{ $ctrl.selectedData.length }} 
row(s) selected</span>
-    </div>
-    <div class="pull-right">
-        <div class="form-inline" role="search">
-            <input id="quickSearch" name="quickSearch" type="search" 
class="form-control text-input" placeholder="Quick search..." 
ng-model="$ctrl.quickSearch" ng-change="$ctrl.onQuickSearchChanged()" 
aria-label="Search"/>
-            <div class="input-group text-input">
-                    <span class="input-group-addon">
-                        <label for="pageSize">Page size</label>
-                    </span>
-                <input id="pageSize" name="pageSize" type="number" min="1" 
class="form-control" placeholder="100" ng-model="$ctrl.pageSize" 
ng-change="$ctrl.onPageSizeChanged()" />
-            </div>
-            <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">
-                    <i class="fa fa-columns"></i>&nbsp;
-                    <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 
$ctrl.gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
-                        <div class="checkbox">
-                            <label><input type="checkbox" 
ng-checked="c.isVisible()" 
ng-click="$ctrl.toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
-                        </div>
-                    </li>
-                </menu>
-            </div>
-            <button class="btn btn-default" title="Refresh" 
ng-if="$ctrl.options.refreshable" ng-click="refresh()"><i class="fa 
fa-refresh"></i></button>
-            <div class="btn-group" role="group" uib-dropdown 
is-open="more.isopen">
-                <button name="moreBtn" type="button" class="btn btn-default 
dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
-                    More&nbsp;
-                    <span class="caret"></span>
-                </button>
-                <ul class="dropdown-menu-right dropdown-menu" 
uib-dropdown-menu>
-                    <li ng-repeat="dd in $ctrl.dropDownOptions" name="{{ 
dd.name }}" role="menuitem" ng-class="{'divider': dd.type == 0}" 
ng-if="$ctrl.isShown(dd)">
-                        <button ng-if="dd.type == 1" class="menu-item-button" 
type="button" ng-click="dd.onClick($ctrl.entry)">{{ $ctrl.getText(dd) 
}}</button>
-                        <a ng-if="dd.type == 2" href="{{ $ctrl.getHref(dd) 
}}">{{ dd.text }}</a>
-                    </li>
-                    <li ng-if="$ctrl.dropDownOptions.length > 0" 
class="divider"></li>
-                    <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="$ctrl.clearTableFilters()">Clear Table 
Filters</button></li>
-                    <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="$ctrl.exportCSV()">Export CSV</button></li>
-                </ul>
-            </div>
-        </div>
-    </div>
-    <div class="clearfix"></div>
+       <div class="pull-left">
+               <ol class="breadcrumb pull-left" ng-if="$ctrl.breadCrumbs !== 
undefined">
+                       <li ng-repeat="bc in $ctrl.breadCrumbs track by $index" 
ng-class="{'active': ($index+1 === $ctrl.breadCrumbs.length)}">
+                               <a ng-if="$ctrl.bcHasHref(bc)" ng-href="{{ 
$ctrl.bcGetHref(bc) }}">{{ $ctrl.bcGetText(bc) }}</a>
+                               <span class="bc" 
ng-if="!$ctrl.bcHasHref(bc)">{{  $ctrl.bcGetText(bc) }}</span>
+                       </li>
+               </ol>
+               <ol class="breadcrumb pull-left" ng-if="$ctrl.breadCrumbs === 
undefined || $ctrl.breadCrumbs.length < 1">
+                       <li class="active" ng-if="$ctrl.tableTitle">
+                               {{ $ctrl.tableTitle }}
+                       </li>
+               </ol>
+               <button ng-if="$ctrl.titleButton !== undefined" type="button" 
class="btn btn-link"
+                               ng-click="$ctrl.titleButton.onClick()">{{ 
$ctrl.titleButton.getText() }}</button>
+               <span ng-if="$ctrl.options.selectRows">{{ 
$ctrl.selectedData.length }} row(s) selected</span>
+       </div>
+       <div class="pull-right">
+               <div class="form-inline" role="search">
+                       <input id="quickSearch" name="quickSearch" 
type="search" class="form-control text-input" placeholder="Quick search..." 
ng-model="$ctrl.quickSearch" ng-change="$ctrl.onQuickSearchChanged()" 
aria-label="Search"/>
+                       <div class="input-group text-input">
+                                       <span class="input-group-addon">
+                                               <label for="pageSize">Page 
size</label>
+                                       </span>
+                               <input id="pageSize" name="pageSize" 
type="number" min="1" class="form-control" placeholder="100" 
ng-model="$ctrl.pageSize" ng-change="$ctrl.onPageSizeChanged()" />
+                       </div>
+                       <div class="input-group text-input" 
ng-if="$ctrl.hasSensitiveColumns()">
+                               <label for="showSensitive" style="color: #333; 
font-size: 14px">Show Sensitive Data Columns</label>
+                               <input id="showSensitive" name="showSensitive" 
type="checkbox" ng-model="$ctrl.sensitiveColumnsShown" 
ng-change="$ctrl.toggleSensitiveFields()"/>
+                       </div>
+                       <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">
+                                       <i class="fa fa-columns"></i>&nbsp;
+                                       <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 
$ctrl.getColumns() | orderBy:'colDef.headerName'">
+                                               <div class="checkbox">
+                                                       <label><input 
type="checkbox" ng-checked="c.isVisible()" 
ng-click="$ctrl.toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
+                                               </div>
+                                       </li>
+                               </menu>
+                       </div>
+                       <button class="btn btn-default" title="Refresh" 
ng-if="$ctrl.options.refreshable" ng-click="refresh()"><i class="fa 
fa-refresh"></i></button>
+                       <div class="btn-group" role="group" uib-dropdown 
is-open="more.isopen">
+                               <button name="moreBtn" type="button" class="btn 
btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" 
aria-expanded="false">
+                                       More&nbsp;
+                                       <span class="caret"></span>
+                               </button>
+                               <ul class="dropdown-menu-right dropdown-menu" 
uib-dropdown-menu>
+                                       <li ng-repeat="dd in 
$ctrl.dropDownOptions" name="{{ dd.name }}" role="menuitem" 
ng-class="{'divider': dd.type == 0}" ng-if="$ctrl.isShown(dd)">
+                                               <button ng-if="dd.type == 1" 
class="menu-item-button" type="button" ng-click="dd.onClick($ctrl.entry)">{{ 
$ctrl.getText(dd) }}</button>
+                                               <a ng-if="dd.type == 2" 
href="{{ $ctrl.getHref(dd) }}">{{ dd.text }}</a>
+                                       </li>
+                                       <li ng-if="$ctrl.dropDownOptions.length 
> 0" class="divider"></li>
+                                       <li role="menuitem"><button 
class="menu-item-button" type="button" 
ng-click="$ctrl.clearTableFilters()">Clear Table Filters</button></li>
+                                       <li role="menuitem"><button 
class="menu-item-button" type="button" ng-click="$ctrl.exportCSV()">Export 
CSV</button></li>
+                               </ul>
+                       </div>
+               </div>
+       </div>
+       <div class="clearfix"></div>
 </div>
 <div class="x_content">
-    <div style="height: 740px;" ag-grid="$ctrl.gridOptions" class="jobs-table 
ag-theme-alpine"></div>
+       <div style="height: 740px;" ag-grid="$ctrl.gridOptions" 
class="jobs-table ag-theme-alpine"></div>
 </div>
 
 <menu id="context-menu" class="dropdown-menu" ng-style="$ctrl.menuStyle" 
type="contextmenu" ng-show="$ctrl.showMenu">
-    <ul>
-        <li role="menuitem" ng-repeat="menu in $ctrl.contextMenuOptions" 
ng-if="$ctrl.isShown(menu)">
-            <hr ng-if="menu.type == 0" class="divider"/>
-            <button ng-if="menu.type == 1" type="button" 
ng-disabled="$ctrl.contextIsDisabled(menu)" type="button" 
ng-click="$ctrl.contextMenuClick(menu, $event)">{{ $ctrl.getText(menu) 
}}</button>
-            <a ng-if="menu.type == 2 && !$ctrl.contextIsDisabled(menu)" 
href="{{ $ctrl.getHref(menu) }}" target="{{ menu.newTab ? '_blank' : '' }}">{{ 
$ctrl.getText(menu) }}</a>
-            <button ng-if="menu.type == 2 && $ctrl.contextIsDisabled(menu)" 
type="button" disabled>{{ $ctrl.getText(menu) }}</a>
-        </li>
-    </ul>
+       <ul>
+               <li role="menuitem" ng-repeat="menu in 
$ctrl.contextMenuOptions" ng-if="$ctrl.isShown(menu)">
+                       <hr ng-if="menu.type == 0" class="divider"/>
+                       <button ng-if="menu.type == 1" type="button" 
ng-disabled="$ctrl.contextIsDisabled(menu)" type="button" 
ng-click="$ctrl.contextMenuClick(menu, $event)">{{ $ctrl.getText(menu) 
}}</button>
+                       <a ng-if="menu.type == 2 && 
!$ctrl.contextIsDisabled(menu)" href="{{ $ctrl.getHref(menu) }}" target="{{ 
menu.newTab ? '_blank' : '' }}">{{ $ctrl.getText(menu) }}</a>
+                       <button ng-if="menu.type == 2 && 
$ctrl.contextIsDisabled(menu)" type="button" disabled>{{ $ctrl.getText(menu) 
}}</a>
+               </li>
+       </ul>
 </menu>
diff --git 
a/traffic_portal/app/src/common/modules/table/cdnDeliveryServices/TableCDNDeliveryServicesController.js
 
b/traffic_portal/app/src/common/modules/table/cdnDeliveryServices/TableCDNDeliveryServicesController.js
index 23a22e02c6..5c650c9fa1 100644
--- 
a/traffic_portal/app/src/common/modules/table/cdnDeliveryServices/TableCDNDeliveryServicesController.js
+++ 
b/traffic_portal/app/src/common/modules/table/cdnDeliveryServices/TableCDNDeliveryServicesController.js
@@ -17,12 +17,25 @@
  * under the License.
  */
 
-var TableCDNDeliveryServicesController = function(cdn, deliveryServices, 
filter, $controller, $scope) {
+function TableCDNDeliveryServicesController(cdn, deliveryServices, filter, 
$controller, $scope) {
 
        // extends the TableDeliveryServicesController to inherit common methods
        angular.extend(this, $controller('TableDeliveryServicesController', { 
tableName: 'cdnDS', deliveryServices: deliveryServices, filter: filter, $scope: 
$scope }));
 
        $scope.cdn = cdn;
+       $scope.breadCrumbs = [
+               {
+                       href: "#!/cdns",
+                       text: "CDNs"
+               },
+               {
+                       href: `#!/cdns/${cdn.id}`,
+                       text: cdn.name
+               },
+               {
+                       text: "Delivery Services"
+               }
+       ];
 };
 
 TableCDNDeliveryServicesController.$inject = ['cdn', 'deliveryServices', 
'filter', '$controller', '$scope'];
diff --git 
a/traffic_portal/app/src/common/modules/table/cdnDeliveryServices/table.cdnDeliveryServices.tpl.html
 
b/traffic_portal/app/src/common/modules/table/cdnDeliveryServices/table.cdnDeliveryServices.tpl.html
index 00dd7961f1..1f85e4352f 100644
--- 
a/traffic_portal/app/src/common/modules/table/cdnDeliveryServices/table.cdnDeliveryServices.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/table/cdnDeliveryServices/table.cdnDeliveryServices.tpl.html
@@ -18,103 +18,14 @@ under the License.
 -->
 
 <div class="x_panel">
-    <div class="x_title">
-        <ol class="breadcrumb pull-left">
-            <li><a href="#!/cdns">CDNs</a></li>
-            <li><a ng-href="#!/cdns/{{cdn.id}}">{{::cdn.name}}</a></li>
-            <li class="active">Delivery Services</li>
-        </ol>
-        <div class="pull-right">
-            <div class="form-inline" role="search">
-                <input id="quickSearch" name="quickSearch" type="search" 
class="form-control text-input" placeholder="Quick search..." 
ng-model="quickSearch" ng-change="onQuickSearchChanged()" aria-label="Search"/>
-                <div class="input-group text-input">
-                    <span class="input-group-addon">
-                        <label for="pageSize">Page size</label>
-                    </span>
-                    <input id="pageSize" name="pageSize" type="number" min="1" 
class="form-control" placeholder="100" ng-model="pageSize" 
ng-change="onPageSizeChanged()" aria-label="Page Size"/>
-                </div>
-                <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">
-                        <i class="fa fa-columns"></i>&nbsp;
-                        <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 
gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
-                            <div class="checkbox">
-                                <label><input type="checkbox" 
ng-checked="c.isVisible()" 
ng-click="toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
-                            </div>
-                        </li>
-                    </menu>
-                </div>
-                <div class="btn-group" role="group" uib-dropdown 
is-open="more.isopen">
-                    <button name="moreBtn" type="button" class="btn 
btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" 
aria-expanded="false">
-                        More&nbsp;
-                        <span class="caret"></span>
-                    </button>
-                    <ul class="dropdown-menu-right dropdown-menu" 
uib-dropdown-menu>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="clearTableFilters()">Clear Table Filters</button></li>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="exportCSV()">Export CSV</button></li>
-                    </ul>
-                </div>
-            </div>
-        </div>
-        <div class="clearfix"></div>
-    </div>
-    <div class="x_content">
-        <div style="height: 740px;" ag-grid="gridOptions" 
class="ag-theme-alpine"></div>
-    </div>
+       <common-grid-controller
+               bread-crumbs="breadCrumbs"
+               table-name="{{tableName}}"
+               options="options"
+               data="deliveryServices"
+               columns="columns"
+               drop-down-options="dropDownOptions"
+               context-menu-options="contextMenuOptions"
+       >
+       </common-grid-controller>
 </div>
-
-<menu id="context-menu" class="dropdown-menu" ng-style="menuStyle" 
type="contextmenu" ng-show="showMenu">
-    <ul>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}"
 target="_blank">Open {{ deliveryService.xmlId }} in New Tab</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}">Edit</a>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="clone(deliveryService, 
$event)">Clone</button>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="confirmDelete(deliveryService, 
$event)">Delete</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <button type="button" ng-click="viewCharts(deliveryService, 
$event)">View Charts</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/ssl-keys?dsType={{deliveryService.type}}">Manage
 SSL Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/url-sig-keys?dsType={{deliveryService.type}}">Manage
 URL Sig Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/uri-signing-keys?dsType={{deliveryService.type}}">Manage
 URI Signing Keys</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/jobs?dsType={{deliveryService.type}}">Manage
 Invalidation Requests</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') == 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/origins?dsType={{deliveryService.type}}">Manage
 Origins</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/regexes?dsType={{deliveryService.type}}">Manage
 Regexes</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('DNS') != -1 
|| deliveryService.type.indexOf('HTTP') != -1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/required-server-capabilities?dsType={{deliveryService.type}}">Manage
 Required Server Capabilities</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/servers?dsType={{deliveryService.type}}">Manage
 Servers</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') != 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/targets?dsType={{deliveryService.type}}">Manage
 Targets</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/static-dns-entries?dsType={{deliveryService.type}}">Manage
 Static DNS Entries</a>
-        </li>
-    </ul>
-</menu>
diff --git 
a/traffic_portal/app/src/common/modules/table/deliveryServices/table.deliveryServices.tpl.html
 
b/traffic_portal/app/src/common/modules/table/deliveryServices/table.deliveryServices.tpl.html
index b532ce2fbb..dd941b8fa9 100644
--- 
a/traffic_portal/app/src/common/modules/table/deliveryServices/table.deliveryServices.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/table/deliveryServices/table.deliveryServices.tpl.html
@@ -28,3 +28,4 @@ under the License.
                context-menu-options="contextMenuOptions"
        >
        </common-grid-controller>
+</div>
diff --git 
a/traffic_portal/app/src/common/modules/table/serverDeliveryServices/TableServerDeliveryServicesController.js
 
b/traffic_portal/app/src/common/modules/table/serverDeliveryServices/TableServerDeliveryServicesController.js
index 98f0e788a6..8f5397c6d6 100644
--- 
a/traffic_portal/app/src/common/modules/table/serverDeliveryServices/TableServerDeliveryServicesController.js
+++ 
b/traffic_portal/app/src/common/modules/table/serverDeliveryServices/TableServerDeliveryServicesController.js
@@ -1,130 +1,162 @@
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
+ * or more contributor license agreements. See the NOTICE file
  * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
+ * 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
+ * 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
+ * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
  */
 
-var TableServerDeliveryServicesController = function(server, deliveryServices, 
filter, $controller, $scope, $state, $uibModal, dateUtils, 
deliveryServiceUtils, locationUtils, serverUtils, deliveryServiceService, 
serverService) {
+function TableServerDeliveryServicesController(server, deliveryServices, 
filter, $controller, $scope, $uibModal, locationUtils, serverUtils, 
deliveryServiceService, serverService) {
 
        // extends the TableDeliveryServicesController to inherit common methods
-       angular.extend(this, $controller('TableDeliveryServicesController', { 
tableName: 'serverDS', deliveryServices: deliveryServices, filter: filter, 
$scope: $scope }));
+       angular.extend(this, $controller("TableDeliveryServicesController", { 
tableName: "serverDS", deliveryServices, filter, $scope }));
 
-       var removeDeliveryService = function(dsId) {
-               deliveryServiceService.deleteDeliveryServiceServer(dsId, 
$scope.server.id)
-                       .then(
-                               function() {
-                                       $scope.refresh();
-                               }
-                       );
+       server = Array.isArray(server) ? server[0] : server;
+
+       /**
+        * Removes the assignment of a Delivery Service to the table's server.
+        *
+        * @param {number} dsId The ID of the Delivery Service being removed.
+        */
+       async function removeDeliveryService(dsId) {
+               await deliveryServiceService.deleteDeliveryServiceServer(dsId, 
$scope.server.id);
+               $scope.refresh();
        };
 
-       $scope.server = server[0];
+       $scope.breadCrumbs = [
+               {
+                       href: "#!/servers",
+                       text: "Servers"
+               },
+               {
+                       href: `#!/servers/${server.id}`,
+                       text: server.hostName
+               },
+               {
+                       text: "Delivery Services"
+               }
+       ];
 
-       $scope.isEdge = serverUtils.isEdge;
+       $scope.dropDownOptions = [
+               {
+                       onClick: cloneAssignments,
+                       text: "Clone Delivery Service Assignments",
+                       type: 1
+               }
+       ];
 
-       $scope.isOrigin = serverUtils.isOrigin;
+       if (serverUtils.isEdge(server) || serverUtils.isOrigin(server)) {
+               $scope.dropDownOptions.unshift({
+                       onClick: selectDeliveryServices,
+                       text: "Assign Delivery Services",
+                       type: 1
+               });
+       }
 
-       $scope.confirmRemoveDS = function(ds, event) {
-               event.stopPropagation();
-               var params = {
-                       title: 'Remove Delivery Service from Server?',
-                       message: 'Are you sure you want to remove ' + ds.xmlId 
+ ' from this server?'
+       /**
+        * Asks a user for confirmation before removing a Delivery Service 
assignment
+        * from the table's server.
+        *
+        * @param {{id: number; xmlId: string}} ds The Delivery Service being 
removed.
+        */
+       async function confirmRemoveDS(ds) {
+               const params = {
+                       title: "Remove Delivery Service from Server?",
+                       message: `Are you sure you want to remove ${ds.xmlId} 
from this server?`
                };
-               var modalInstance = $uibModal.open({
-                       templateUrl: 
'common/modules/dialog/confirm/dialog.confirm.tpl.html',
-                       controller: 'DialogConfirmController',
-                       size: 'md',
-                       resolve: {
-                               params: function () {
-                                       return params;
-                               }
-                       }
+               const modalInstance = $uibModal.open({
+                       templateUrl: 
"common/modules/dialog/confirm/dialog.confirm.tpl.html",
+                       controller: "DialogConfirmController",
+                       size: "md",
+                       resolve: { params }
                });
-               modalInstance.result.then(function() {
+               try {
+                       await modalInstance.result;
                        removeDeliveryService(ds.id);
-               }, function () {
+               } catch {
                        // do nothing
-               });
+               }
        };
 
-       $scope.cloneDsAssignments = function() {
-               var params = {
-                       title: 'Clone Delivery Service Assignments',
-                       message: "Please select another " + $scope.server.type 
+ " cache to assign these " + deliveryServices.length + " delivery services 
to." +
+       $scope.contextMenuOptions.splice(1, 0, {
+               onClick: confirmRemoveDS,
+               getText: ds => `Remove ${ds.xmlId}`,
+               type: 1
+       });
+
+       async function cloneAssignments() {
+               const params = {
+                       title: "Clone Delivery Service Assignments",
+                       message: `Please select another ${server.type} cache to 
which to assign these ${deliveryServices.length} Delivery Services.` +
                                "<br>" +
                                "<br>" +
-                               "<strong>WARNING THIS CANNOT BE UNDONE</strong> 
- Any delivery services currently assigned to the selected cache will be lost 
and replaced with these " + deliveryServices.length + " delivery service 
assignments.",
-                       labelFunction: function(item) { return item['hostName'] 
+ '.' + item['domainName'] }
+                               `<strong class="uppercase">Warning this cannot 
be undone</strong> - Any Delivery Services currently assigned to the selected 
cache will be lost and replaced with these ${deliveryServices.length} Delivery 
Service assignments.`,
+                       labelFunction: item => 
`${item.hostName}.${item.domainName}`
                };
-               var modalInstance = $uibModal.open({
-                       templateUrl: 
'common/modules/dialog/select/dialog.select.tpl.html',
-                       controller: 'DialogSelectController',
-                       size: 'md',
+
+               const modalInstance = $uibModal.open({
+                       templateUrl: 
"common/modules/dialog/select/dialog.select.tpl.html",
+                       controller: "DialogSelectController",
+                       size: "md",
                        resolve: {
-                               params: function () {
-                                       return params;
-                               },
-                               collection: function(serverService) {
-                                       return serverService.getServers({ type: 
$scope.server.type, orderby: 'hostName', cdn: $scope.server.cdnId 
}).then(function(xs){return xs.filter(function(x){return 
x.id!=$scope.server.id})}, function(err){throw err});
+                               params,
+                               collection: async () => {
+                                       const opts = {
+                                               type: server.type,
+                                               orderby: "hostName",
+                                               cdn: server.cdnId
+                                       };
+                                       const ss = await 
serverService.getServers(opts);
+                                       return ss.filter(s => s.id !== 
server.id);
                                }
                        }
                });
-               modalInstance.result.then(function(selectedServer) {
-                       var dsIds = _.pluck(deliveryServices, 'id');
-                       serverService.assignDeliveryServices(selectedServer, 
dsIds, true, true)
-                               .then(
-                                       function() {
-                                               
locationUtils.navigateToPath('/servers/' + selectedServer.id + 
'/delivery-services');
-                                       }
-                               );
-               }, function () {
-                       // do nothing
-               });
+
+               let selectedServer;
+               try {
+                       selectedServer = await modalInstance.result;
+               } catch {
+                       return;
+               }
+               const dsIds = deliveryServices.map(ds=>ds.id);
+               await serverService.assignDeliveryServices(selectedServer, 
dsIds, true, true);
+               
locationUtils.navigateToPath(`/servers/${selectedServer.id}/delivery-services`);
        };
 
-       $scope.selectDeliveryServices = function() {
-               var modalInstance = $uibModal.open({
-                       templateUrl: 
'common/modules/table/serverDeliveryServices/table.assignDeliveryServices.tpl.html',
-                       controller: 'TableAssignDeliveryServicesController',
-                       size: 'lg',
+       async function selectDeliveryServices() {
+               const modalInstance = $uibModal.open({
+                       templateUrl: 
"common/modules/table/serverDeliveryServices/table.assignDeliveryServices.tpl.html",
+                       controller: "TableAssignDeliveryServicesController",
+                       size: "lg",
                        resolve: {
-                               server: function() {
-                                       return $scope.server;
-                               },
-                               deliveryServices: 
function(deliveryServiceService) {
-                                       return 
deliveryServiceService.getDeliveryServices({ cdn: $scope.server.cdnId });
-                               },
-                               assignedDeliveryServices: function() {
-                                       return deliveryServices;
-                               }
+                               server: () => server,
+                               deliveryServices: deliveryServiceService => 
deliveryServiceService.getDeliveryServices({ cdn: server.cdnId }),
+                               assignedDeliveryServices: () => deliveryServices
                        }
                });
-               modalInstance.result.then(function(selectedDsIds) {
-                       serverService.assignDeliveryServices($scope.server, 
selectedDsIds, true, false)
-                               .then(
-                                       function() {
-                                               $scope.refresh();
-                                       }
-                               );
-               }, function () {
-                       // do nothing
-               });
+
+               let selectedDSIDs;
+               try {
+                       selectedDSIDs = await modalInstance.result;
+               } catch {
+                       return;
+               }
+               await serverService.assignDeliveryServices(server, 
selectedDSIDs, true, false);
+               $scope.refresh();
        };
 
 };
 
-TableServerDeliveryServicesController.$inject = ['server', 'deliveryServices', 
'filter', '$controller', '$scope', '$state', '$uibModal', 'dateUtils', 
'deliveryServiceUtils', 'locationUtils', 'serverUtils', 
'deliveryServiceService', 'serverService'];
+TableServerDeliveryServicesController.$inject = ["server", "deliveryServices", 
"filter", "$controller", "$scope", "$uibModal", "locationUtils", "serverUtils", 
"deliveryServiceService", "serverService"];
 module.exports = TableServerDeliveryServicesController;
diff --git 
a/traffic_portal/app/src/common/modules/table/serverDeliveryServices/table.serverDeliveryServices.tpl.html
 
b/traffic_portal/app/src/common/modules/table/serverDeliveryServices/table.serverDeliveryServices.tpl.html
index c182756fff..1f85e4352f 100644
--- 
a/traffic_portal/app/src/common/modules/table/serverDeliveryServices/table.serverDeliveryServices.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/table/serverDeliveryServices/table.serverDeliveryServices.tpl.html
@@ -18,110 +18,14 @@ under the License.
 -->
 
 <div class="x_panel">
-    <div class="x_title">
-        <ol class="breadcrumb pull-left">
-            <li><a href="#!/servers">Servers</a></li>
-            <li><a 
ng-href="#!/servers/{{server.id}}">{{::server.hostName}}</a></li>
-            <li class="active">Delivery Services</li>
-        </ol>
-        <div class="pull-right">
-            <div class="form-inline" role="search">
-                <input id="quickSearch" name="quickSearch" type="search" 
class="form-control text-input" placeholder="Quick search..." 
ng-model="quickSearch" ng-change="onQuickSearchChanged()" aria-label="Search"/>
-                <div class="input-group text-input">
-                    <span class="input-group-addon">
-                        <label for="pageSize">Page size</label>
-                    </span>
-                    <input id="pageSize" name="pageSize" type="number" min="1" 
class="form-control" placeholder="100" ng-model="pageSize" 
ng-change="onPageSizeChanged()" aria-label="Page Size"/>
-                </div>
-                <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">
-                        <i class="fa fa-columns"></i>&nbsp;
-                        <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 
gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
-                            <div class="checkbox">
-                                <label><input type="checkbox" 
ng-checked="c.isVisible()" 
ng-click="toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
-                            </div>
-                        </li>
-                    </menu>
-                </div>
-                <div class="btn-group" role="group" uib-dropdown 
is-open="more.isopen">
-                    <button name="moreBtn" type="button" class="btn 
btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" 
aria-expanded="false">
-                        More&nbsp;
-                        <span class="caret"></span>
-                    </button>
-                    <ul class="dropdown-menu-right dropdown-menu" 
uib-dropdown-menu>
-                        <li role="menuitem" ng-show="isEdge(server) || 
isOrigin(server)"><button class="menu-item-button clone-ds-assignments" 
type="button" ng-click="selectDeliveryServices()">Assign Delivery 
Services</button></li>
-                        <li role="menuitem"><button class="menu-item-button 
clone-ds-assignments" type="button" ng-click="cloneDsAssignments()">Clone 
Delivery Service Assignments</button></li>
-                        <li class="divider"></li>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="clearTableFilters()">Clear Table Filters</button></li>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="exportCSV()">Export CSV</button></li>
-                    </ul>
-                </div>
-            </div>
-        </div>
-        <div class="clearfix"></div>
-    </div>
-    <div class="x_content">
-        <div style="height: 740px;" ag-grid="gridOptions" 
class="ag-theme-alpine"></div>
-    </div>
+       <common-grid-controller
+               bread-crumbs="breadCrumbs"
+               table-name="{{tableName}}"
+               options="options"
+               data="deliveryServices"
+               columns="columns"
+               drop-down-options="dropDownOptions"
+               context-menu-options="contextMenuOptions"
+       >
+       </common-grid-controller>
 </div>
-
-<menu id="context-menu" class="dropdown-menu" ng-style="menuStyle" 
type="contextmenu" ng-show="showMenu">
-    <ul>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}"
 target="_blank">Open {{ deliveryService.xmlId }} in New Tab</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <button type="button" ng-click="confirmRemoveDS(deliveryService, 
$event)">Remove {{deliveryService.xmlId}}</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}">Edit</a>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="clone(deliveryService, 
$event)">Clone</button>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="confirmDelete(deliveryService, 
$event)">Delete</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <button type="button" ng-click="viewCharts(deliveryService, 
$event)">View Charts</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/ssl-keys?dsType={{deliveryService.type}}">Manage
 SSL Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/url-sig-keys?dsType={{deliveryService.type}}">Manage
 URL Sig Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/uri-signing-keys?dsType={{deliveryService.type}}">Manage
 URI Signing Keys</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/jobs?dsType={{deliveryService.type}}">Manage
 Invalidation Requests</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') == 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/origins?dsType={{deliveryService.type}}">Manage
 Origins</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/regexes?dsType={{deliveryService.type}}">Manage
 Regexes</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('DNS') != -1 
|| deliveryService.type.indexOf('HTTP') != -1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/required-server-capabilities?dsType={{deliveryService.type}}">Manage
 Required Server Capabilities</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/servers?dsType={{deliveryService.type}}">Manage
 Servers</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') != 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/targets?dsType={{deliveryService.type}}">Manage
 Targets</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/static-dns-entries?dsType={{deliveryService.type}}">Manage
 Static DNS Entries</a>
-        </li>
-    </ul>
-</menu>
diff --git 
a/traffic_portal/app/src/common/modules/table/serviceCategoryDeliveryServices/TableServiceCategoryDeliveryServicesController.js
 
b/traffic_portal/app/src/common/modules/table/serviceCategoryDeliveryServices/TableServiceCategoryDeliveryServicesController.js
index cb70a7d3fa..36aff62dc5 100644
--- 
a/traffic_portal/app/src/common/modules/table/serviceCategoryDeliveryServices/TableServiceCategoryDeliveryServicesController.js
+++ 
b/traffic_portal/app/src/common/modules/table/serviceCategoryDeliveryServices/TableServiceCategoryDeliveryServicesController.js
@@ -23,6 +23,19 @@ var TableServiceCategoryDeliveryServicesController = 
function(serviceCategory, d
        angular.extend(this, $controller('TableDeliveryServicesController', { 
tableName: 'scDS', deliveryServices: deliveryServices, filter: filter, $scope: 
$scope }));
 
        $scope.serviceCategory = serviceCategory;
+       $scope.breadCrumbs = [
+               {
+                       text: "Service Categories",
+                       href: "#!/service-categories"
+               },
+               {
+                       getText: () => serviceCategory.name,
+                       getHref: () => 
`#!/service-categories/edit?name=${serviceCategory.name}`
+               },
+               {
+                       text: "Delivery Services"
+               }
+       ]
 };
 
 TableServiceCategoryDeliveryServicesController.$inject = ['serviceCategory', 
'deliveryServices', 'filter', '$controller', '$scope'];
diff --git 
a/traffic_portal/app/src/common/modules/table/serviceCategoryDeliveryServices/table.serviceCategoryDeliveryServices.tpl.html
 
b/traffic_portal/app/src/common/modules/table/serviceCategoryDeliveryServices/table.serviceCategoryDeliveryServices.tpl.html
index f569543c4d..2d9dfa50be 100644
--- 
a/traffic_portal/app/src/common/modules/table/serviceCategoryDeliveryServices/table.serviceCategoryDeliveryServices.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/table/serviceCategoryDeliveryServices/table.serviceCategoryDeliveryServices.tpl.html
@@ -18,105 +18,15 @@ under the License.
 -->
 
 <div class="x_panel">
-    <div class="x_title">
-        <ol class="breadcrumb pull-left">
-            <li><a href="#!/service-categories">Service Categories</a></li>
-            <li><a 
ng-href="#!/service-categories/edit?name={{serviceCategory.name}}">{{::serviceCategory.name}}</a></li>
-            <li class="active">Delivery Services</li>
-        </ol>
-        <div class="pull-right">
-            <div class="form-inline" role="search">
-                <input id="quickSearch" name="quickSearch" type="search" 
class="form-control text-input" placeholder="Quick search..." 
ng-model="quickSearch" ng-change="onQuickSearchChanged()" aria-label="Search"/>
-                <div class="input-group text-input">
-                    <span class="input-group-addon">
-                        <label for="pageSize">Page size</label>
-                    </span>
-                    <input id="pageSize" name="pageSize" type="number" min="1" 
class="form-control" placeholder="100" ng-model="pageSize" 
ng-change="onPageSizeChanged()" aria-label="Page Size"/>
-                </div>
-                <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">
-                        <i class="fa fa-columns"></i>&nbsp;
-                        <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 
gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
-                            <div class="checkbox">
-                                <label><input type="checkbox" 
ng-checked="c.isVisible()" 
ng-click="toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
-                            </div>
-                        </li>
-                    </menu>
-                </div>
-                <div class="btn-group" role="group" uib-dropdown 
is-open="more.isopen">
-                    <button name="moreBtn" type="button" class="btn 
btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" 
aria-expanded="false">
-                        More&nbsp;
-                        <span class="caret"></span>
-                    </button>
-                    <ul class="dropdown-menu-right dropdown-menu" 
uib-dropdown-menu>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="clearTableFilters()">Clear Table Filters</button></li>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="exportCSV()">Export CSV</button></li>
-                    </ul>
-                </div>
-            </div>
-        </div>
-        <div class="clearfix"></div>
-    </div>
-    <div class="x_content">
-        <div style="height: 740px;" ag-grid="gridOptions" 
class="ag-theme-alpine"></div>
-    </div>
+       <common-grid-controller
+               bread-crumbs="breadCrumbs"
+               table-name="{{tableName}}"
+               options="options"
+               data="deliveryServices"
+               columns="columns"
+               drop-down-options="dropDownOptions"
+               context-menu-options="contextMenuOptions"
+               sensitive-columns="sensitiveColumns"
+       >
+       </common-grid-controller>
 </div>
-
-<menu id="context-menu" class="dropdown-menu" ng-style="menuStyle" 
type="contextmenu" ng-show="showMenu">
-    <ul>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}"
 target="_blank">Open {{ deliveryService.xmlId }} in New Tab</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}">Edit</a>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="clone(deliveryService, 
$event)">Clone</button>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="confirmDelete(deliveryService, 
$event)">Delete</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <button type="button" ng-click="viewCharts(deliveryService, 
$event)">View Charts</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/ssl-keys?dsType={{deliveryService.type}}">Manage
 SSL Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/url-sig-keys?dsType={{deliveryService.type}}">Manage
 URL Sig Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/uri-signing-keys?dsType={{deliveryService.type}}">Manage
 URI Signing Keys</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/jobs?dsType={{deliveryService.type}}">Manage
 Invalidation Requests</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') == 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/origins?dsType={{deliveryService.type}}">Manage
 Origins</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/regexes?dsType={{deliveryService.type}}">Manage
 Regexes</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('DNS') != -1 
|| deliveryService.type.indexOf('HTTP') != -1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/required-server-capabilities?dsType={{deliveryService.type}}">Manage
 Required Server Capabilities</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/servers?dsType={{deliveryService.type}}">Manage
 Servers</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') != 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/targets?dsType={{deliveryService.type}}">Manage
 Targets</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/static-dns-entries?dsType={{deliveryService.type}}">Manage
 Static DNS Entries</a>
-        </li>
-    </ul>
-</menu>
-
-
diff --git 
a/traffic_portal/app/src/common/modules/table/tenantDeliveryServices/TableTenantDeliveryServicesController.js
 
b/traffic_portal/app/src/common/modules/table/tenantDeliveryServices/TableTenantDeliveryServicesController.js
index c8c8357531..8277426a81 100644
--- 
a/traffic_portal/app/src/common/modules/table/tenantDeliveryServices/TableTenantDeliveryServicesController.js
+++ 
b/traffic_portal/app/src/common/modules/table/tenantDeliveryServices/TableTenantDeliveryServicesController.js
@@ -23,6 +23,19 @@ var TableTenantDeliveryServicesController = function(tenant, 
deliveryServices, f
        angular.extend(this, $controller('TableDeliveryServicesController', { 
tableName: 'tenantDS', deliveryServices: deliveryServices, filter: filter, 
$scope: $scope }));
 
        $scope.tenant = tenant;
+       $scope.breadCrumbs = [
+               {
+                       href: "#!/tenants",
+                       text: "Tenants"
+               },
+               {
+                       getText: () => tenant.name,
+                       getHref: () => `#!/tenants/${tenant.id}`
+               },
+               {
+                       text: "Delivery Services"
+               }
+       ];
 };
 
 TableTenantDeliveryServicesController.$inject = ['tenant', 'deliveryServices', 
'filter', '$controller', '$scope'];
diff --git 
a/traffic_portal/app/src/common/modules/table/tenantDeliveryServices/table.tenantDeliveryServices.tpl.html
 
b/traffic_portal/app/src/common/modules/table/tenantDeliveryServices/table.tenantDeliveryServices.tpl.html
index 5ed26baba7..2d9dfa50be 100644
--- 
a/traffic_portal/app/src/common/modules/table/tenantDeliveryServices/table.tenantDeliveryServices.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/table/tenantDeliveryServices/table.tenantDeliveryServices.tpl.html
@@ -18,103 +18,15 @@ under the License.
 -->
 
 <div class="x_panel">
-    <div class="x_title">
-        <ol class="breadcrumb pull-left">
-            <li><a href="#!/tenants">Tenants</a></li>
-            <li><a 
ng-href="#!/tenants/{{tenant.id}}">{{::tenant.name}}</a></li>
-            <li class="active">Delivery Services</li>
-        </ol>
-        <div class="pull-right">
-            <div class="form-inline" role="search">
-                <input id="quickSearch" name="quickSearch" type="search" 
class="form-control text-input" placeholder="Quick search..." 
ng-model="quickSearch" ng-change="onQuickSearchChanged()" aria-label="Search"/>
-                <div class="input-group text-input">
-                    <span class="input-group-addon">
-                        <label for="pageSize">Page size</label>
-                    </span>
-                    <input id="pageSize" name="pageSize" type="number" min="1" 
class="form-control" placeholder="100" ng-model="pageSize" 
ng-change="onPageSizeChanged()" aria-label="Page Size"/>
-                </div>
-                <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">
-                        <i class="fa fa-columns"></i>&nbsp;
-                        <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 
gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
-                            <div class="checkbox">
-                                <label><input type="checkbox" 
ng-checked="c.isVisible()" 
ng-click="toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
-                            </div>
-                        </li>
-                    </menu>
-                </div>
-                <div class="btn-group" role="group" uib-dropdown 
is-open="more.isopen">
-                    <button name="moreBtn" type="button" class="btn 
btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" 
aria-expanded="false">
-                        More&nbsp;
-                        <span class="caret"></span>
-                    </button>
-                    <ul class="dropdown-menu-right dropdown-menu" 
uib-dropdown-menu>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="clearTableFilters()">Clear Table Filters</button></li>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="exportCSV()">Export CSV</button></li>
-                    </ul>
-                </div>
-            </div>
-        </div>
-        <div class="clearfix"></div>
-    </div>
-    <div class="x_content">
-        <div style="height: 740px;" ag-grid="gridOptions" 
class="ag-theme-alpine"></div>
-    </div>
+       <common-grid-controller
+               bread-crumbs="breadCrumbs"
+               table-name="{{tableName}}"
+               options="options"
+               data="deliveryServices"
+               columns="columns"
+               drop-down-options="dropDownOptions"
+               context-menu-options="contextMenuOptions"
+               sensitive-columns="sensitiveColumns"
+       >
+       </common-grid-controller>
 </div>
-
-<menu id="context-menu" class="dropdown-menu" ng-style="menuStyle" 
type="contextmenu" ng-show="showMenu">
-    <ul>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}"
 target="_blank">Open {{ deliveryService.xmlId }} in New Tab</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}">Edit</a>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="clone(deliveryService, 
$event)">Clone</button>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="confirmDelete(deliveryService, 
$event)">Delete</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <button type="button" ng-click="viewCharts(deliveryService, 
$event)">View Charts</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/ssl-keys?dsType={{deliveryService.type}}">Manage
 SSL Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/url-sig-keys?dsType={{deliveryService.type}}">Manage
 URL Sig Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/uri-signing-keys?dsType={{deliveryService.type}}">Manage
 URI Signing Keys</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/jobs?dsType={{deliveryService.type}}">Manage
 Invalidation Requests</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') == 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/origins?dsType={{deliveryService.type}}">Manage
 Origins</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/regexes?dsType={{deliveryService.type}}">Manage
 Regexes</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('DNS') != -1 
|| deliveryService.type.indexOf('HTTP') != -1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/required-server-capabilities?dsType={{deliveryService.type}}">Manage
 Required Server Capabilities</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/servers?dsType={{deliveryService.type}}">Manage
 Servers</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') != 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/targets?dsType={{deliveryService.type}}">Manage
 Targets</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/static-dns-entries?dsType={{deliveryService.type}}">Manage
 Static DNS Entries</a>
-        </li>
-    </ul>
-</menu>
diff --git 
a/traffic_portal/app/src/common/modules/table/topologyDeliveryServices/TableTopologyDeliveryServicesController.js
 
b/traffic_portal/app/src/common/modules/table/topologyDeliveryServices/TableTopologyDeliveryServicesController.js
index c2719dd7ab..2168ab7ab9 100644
--- 
a/traffic_portal/app/src/common/modules/table/topologyDeliveryServices/TableTopologyDeliveryServicesController.js
+++ 
b/traffic_portal/app/src/common/modules/table/topologyDeliveryServices/TableTopologyDeliveryServicesController.js
@@ -17,13 +17,26 @@
  * under the License.
  */
 
-var TableTopologyDeliveryServicesController = function(topologies, 
deliveryServices, filter, $controller, $scope) {
+function TableTopologyDeliveryServicesController(topologies, deliveryServices, 
filter, $controller, $scope) {
 
        // extends the TableDeliveryServicesController to inherit common methods
-       angular.extend(this, $controller('TableDeliveryServicesController', { 
tableName: 'topDS', deliveryServices: deliveryServices, filter: filter, $scope: 
$scope }));
+       angular.extend(this, $controller("TableDeliveryServicesController", { 
tableName: "topDS", deliveryServices, filter, $scope }));
 
-       $scope.topology = topologies[0];
+       const topology = topologies[0];
+       $scope.breadCrumbs = [
+               {
+                       href: "#!/topologies",
+                       text: "Topologies"
+               },
+               {
+                       href: `#!/topologies/edit?name=${topology.name}`,
+                       text: topology.name
+               },
+               {
+                       text: "Delivery Services"
+               }
+       ];
 };
 
-TableTopologyDeliveryServicesController.$inject = ['topologies', 
'deliveryServices', 'filter', '$controller', '$scope'];
+TableTopologyDeliveryServicesController.$inject = ["topologies", 
"deliveryServices", "filter", "$controller", "$scope"];
 module.exports = TableTopologyDeliveryServicesController;
diff --git 
a/traffic_portal/app/src/common/modules/table/topologyDeliveryServices/table.topologyDeliveryServices.tpl.html
 
b/traffic_portal/app/src/common/modules/table/topologyDeliveryServices/table.topologyDeliveryServices.tpl.html
index ed10084321..2d9dfa50be 100644
--- 
a/traffic_portal/app/src/common/modules/table/topologyDeliveryServices/table.topologyDeliveryServices.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/table/topologyDeliveryServices/table.topologyDeliveryServices.tpl.html
@@ -18,103 +18,15 @@ under the License.
 -->
 
 <div class="x_panel">
-    <div class="x_title">
-        <ol class="breadcrumb pull-left">
-            <li><a href="#!/topologies">Topologies</a></li>
-            <li><a name="topLink" 
ng-href="#!/topologies/edit?name={{topology.name}}">{{::topology.name}}</a></li>
-            <li class="active">Delivery Services</li>
-        </ol>
-        <div class="pull-right">
-            <div class="form-inline" role="search">
-                <input id="quickSearch" name="quickSearch" type="search" 
class="form-control text-input" placeholder="Quick search..." 
ng-model="quickSearch" ng-change="onQuickSearchChanged()" aria-label="Search"/>
-                <div class="input-group text-input">
-                    <span class="input-group-addon">
-                        <label for="pageSize">Page size</label>
-                    </span>
-                    <input id="pageSize" name="pageSize" type="number" min="1" 
class="form-control" placeholder="100" ng-model="pageSize" 
ng-change="onPageSizeChanged()" aria-label="Page Size"/>
-                </div>
-                <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">
-                        <i class="fa fa-columns"></i>&nbsp;
-                        <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 
gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
-                            <div class="checkbox">
-                                <label><input type="checkbox" 
ng-checked="c.isVisible()" 
ng-click="toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
-                            </div>
-                        </li>
-                    </menu>
-                </div>
-                <div class="btn-group" role="group" uib-dropdown 
is-open="more.isopen">
-                    <button name="moreBtn" type="button" class="btn 
btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" 
aria-expanded="false">
-                        More&nbsp;
-                        <span class="caret"></span>
-                    </button>
-                    <ul class="dropdown-menu-right dropdown-menu" 
uib-dropdown-menu>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="clearTableFilters()">Clear Table Filters</button></li>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="exportCSV()">Export CSV</button></li>
-                    </ul>
-                </div>
-            </div>
-        </div>
-        <div class="clearfix"></div>
-    </div>
-    <div class="x_content">
-        <div style="height: 740px;" ag-grid="gridOptions" 
class="ag-theme-alpine"></div>
-    </div>
+       <common-grid-controller
+               bread-crumbs="breadCrumbs"
+               table-name="{{tableName}}"
+               options="options"
+               data="deliveryServices"
+               columns="columns"
+               drop-down-options="dropDownOptions"
+               context-menu-options="contextMenuOptions"
+               sensitive-columns="sensitiveColumns"
+       >
+       </common-grid-controller>
 </div>
-
-<menu id="context-menu" class="dropdown-menu" ng-style="menuStyle" 
type="contextmenu" ng-show="showMenu">
-    <ul>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}"
 target="_blank">Open {{ deliveryService.xmlId }} in New Tab</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}">Edit</a>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="clone(deliveryService, 
$event)">Clone</button>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="confirmDelete(deliveryService, 
$event)">Delete</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <button type="button" ng-click="viewCharts(deliveryService, 
$event)">View Charts</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/ssl-keys?dsType={{deliveryService.type}}">Manage
 SSL Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/url-sig-keys?dsType={{deliveryService.type}}">Manage
 URL Sig Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/uri-signing-keys?dsType={{deliveryService.type}}">Manage
 URI Signing Keys</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/jobs?dsType={{deliveryService.type}}">Manage
 Invalidation Requests</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') == 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/origins?dsType={{deliveryService.type}}">Manage
 Origins</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/regexes?dsType={{deliveryService.type}}">Manage
 Regexes</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('DNS') != -1 
|| deliveryService.type.indexOf('HTTP') != -1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/required-server-capabilities?dsType={{deliveryService.type}}">Manage
 Required Server Capabilities</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/servers?dsType={{deliveryService.type}}">Manage
 Servers</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') != 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/targets?dsType={{deliveryService.type}}">Manage
 Targets</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/static-dns-entries?dsType={{deliveryService.type}}">Manage
 Static DNS Entries</a>
-        </li>
-    </ul>
-</menu>
diff --git 
a/traffic_portal/app/src/common/modules/table/typeDeliveryServices/TableTypeDeliveryServicesController.js
 
b/traffic_portal/app/src/common/modules/table/typeDeliveryServices/TableTypeDeliveryServicesController.js
index 79e15e3e9d..383eff365f 100644
--- 
a/traffic_portal/app/src/common/modules/table/typeDeliveryServices/TableTypeDeliveryServicesController.js
+++ 
b/traffic_portal/app/src/common/modules/table/typeDeliveryServices/TableTypeDeliveryServicesController.js
@@ -1,18 +1,18 @@
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
+ * or more contributor license agreements. See the NOTICE file
  * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
+ * 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
+ * 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
+ * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
  */
@@ -20,10 +20,22 @@
 var TableTypeDeliveryServicesController = function(type, deliveryServices, 
filter, $controller, $scope) {
 
        // extends the TableDeliveryServicesController to inherit common methods
-       angular.extend(this, $controller('TableDeliveryServicesController', { 
tableName: 'typeDS', deliveryServices: deliveryServices, filter: filter, 
$scope: $scope }));
+       angular.extend(this, $controller("TableDeliveryServicesController", { 
tableName: "typeDS", deliveryServices, filter, $scope }));
 
-       $scope.type = type;
+       $scope.breadCrumbs = [
+               {
+                       href: "#!/types",
+                       text: "Types"
+               },
+               {
+                       href: `#!/types/${type.id}`,
+                       text: type.name,
+               },
+               {
+                       text: "Delivery Services"
+               }
+       ]
 };
 
-TableTypeDeliveryServicesController.$inject = ['type', 'deliveryServices', 
'filter', '$controller', '$scope'];
+TableTypeDeliveryServicesController.$inject = ["type", "deliveryServices", 
"filter", "$controller", "$scope"];
 module.exports = TableTypeDeliveryServicesController;
diff --git 
a/traffic_portal/app/src/common/modules/table/typeDeliveryServices/table.typeDeliveryServices.tpl.html
 
b/traffic_portal/app/src/common/modules/table/typeDeliveryServices/table.typeDeliveryServices.tpl.html
index 2ae3920318..2d9dfa50be 100644
--- 
a/traffic_portal/app/src/common/modules/table/typeDeliveryServices/table.typeDeliveryServices.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/table/typeDeliveryServices/table.typeDeliveryServices.tpl.html
@@ -18,103 +18,15 @@ under the License.
 -->
 
 <div class="x_panel">
-    <div class="x_title">
-        <ol class="breadcrumb pull-left">
-            <li><a href="#!/types">Types</a></li>
-            <li><a ng-href="#!/types/{{type.id}}">{{::type.name}}</a></li>
-            <li class="active">Delivery Services</li>
-        </ol>
-        <div class="pull-right">
-            <div class="form-inline" role="search">
-                <input id="quickSearch" name="quickSearch" type="search" 
class="form-control text-input" placeholder="Quick search..." 
ng-model="quickSearch" ng-change="onQuickSearchChanged()" aria-label="Search"/>
-                <div class="input-group text-input">
-                    <span class="input-group-addon">
-                        <label for="pageSize">Page size</label>
-                    </span>
-                    <input id="pageSize" name="pageSize" type="number" min="1" 
class="form-control" placeholder="100" ng-model="pageSize" 
ng-change="onPageSizeChanged()" aria-label="Page Size"/>
-                </div>
-                <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">
-                        <i class="fa fa-columns"></i>&nbsp;
-                        <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 
gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
-                            <div class="checkbox">
-                                <label><input type="checkbox" 
ng-checked="c.isVisible()" 
ng-click="toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
-                            </div>
-                        </li>
-                    </menu>
-                </div>
-                <div class="btn-group" role="group" uib-dropdown 
is-open="more.isopen">
-                    <button name="moreBtn" type="button" class="btn 
btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" 
aria-expanded="false">
-                        More&nbsp;
-                        <span class="caret"></span>
-                    </button>
-                    <ul class="dropdown-menu-right dropdown-menu" 
uib-dropdown-menu>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="clearTableFilters()">Clear Table Filters</button></li>
-                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="exportCSV()">Export CSV</button></li>
-                    </ul>
-                </div>
-            </div>
-        </div>
-        <div class="clearfix"></div>
-    </div>
-    <div class="x_content">
-        <div style="height: 740px;" ag-grid="gridOptions" 
class="ag-theme-alpine"></div>
-    </div>
+       <common-grid-controller
+               bread-crumbs="breadCrumbs"
+               table-name="{{tableName}}"
+               options="options"
+               data="deliveryServices"
+               columns="columns"
+               drop-down-options="dropDownOptions"
+               context-menu-options="contextMenuOptions"
+               sensitive-columns="sensitiveColumns"
+       >
+       </common-grid-controller>
 </div>
-
-<menu id="context-menu" class="dropdown-menu" ng-style="menuStyle" 
type="contextmenu" ng-show="showMenu">
-    <ul>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}"
 target="_blank">Open {{ deliveryService.xmlId }} in New Tab</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}?dsType={{deliveryService.type}}">Edit</a>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="clone(deliveryService, 
$event)">Clone</button>
-        </li>
-        <li role="menuitem">
-            <button type="button" ng-click="confirmDelete(deliveryService, 
$event)">Delete</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <button type="button" ng-click="viewCharts(deliveryService, 
$event)">View Charts</button>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/ssl-keys?dsType={{deliveryService.type}}">Manage
 SSL Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/url-sig-keys?dsType={{deliveryService.type}}">Manage
 URL Sig Keys</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/uri-signing-keys?dsType={{deliveryService.type}}">Manage
 URI Signing Keys</a>
-        </li>
-        <hr class="divider"/>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/jobs?dsType={{deliveryService.type}}">Manage
 Invalidation Requests</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') == 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/origins?dsType={{deliveryService.type}}">Manage
 Origins</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/regexes?dsType={{deliveryService.type}}">Manage
 Regexes</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('DNS') != -1 
|| deliveryService.type.indexOf('HTTP') != -1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/required-server-capabilities?dsType={{deliveryService.type}}">Manage
 Required Server Capabilities</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/servers?dsType={{deliveryService.type}}">Manage
 Servers</a>
-        </li>
-        <li role="menuitem" ng-if="deliveryService.type.indexOf('STEERING') != 
-1">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/targets?dsType={{deliveryService.type}}">Manage
 Targets</a>
-        </li>
-        <li role="menuitem">
-            <a 
ng-href="#!/delivery-services/{{deliveryService.id}}/static-dns-entries?dsType={{deliveryService.type}}">Manage
 Static DNS Entries</a>
-        </li>
-    </ul>
-</menu>
diff --git a/traffic_portal/app/src/styles/main.scss 
b/traffic_portal/app/src/styles/main.scss
index 12f6453111..c4f0e6aab4 100644
--- a/traffic_portal/app/src/styles/main.scss
+++ b/traffic_portal/app/src/styles/main.scss
@@ -269,3 +269,9 @@ input[type="checkbox"].dirty {
   background-color: #d3d3d4;
   border-color: #bcbebf;
 }
+
+// This is, unfortunately, necessary because uibModal doesn't support scoped
+// stylesheets (afaik; it's a dead library so nobody knows).
+.uppercase {
+       text-transform: uppercase;
+}
diff --git a/traffic_portal/test/integration/Data/deliveryservices.ts 
b/traffic_portal/test/integration/Data/deliveryservices.ts
index cc90d8d177..7bd9c48644 100644
--- a/traffic_portal/test/integration/Data/deliveryservices.ts
+++ b/traffic_portal/test/integration/Data/deliveryservices.ts
@@ -299,196 +299,174 @@ export const deliveryservices = {
        ],
        tests: [
                {
-                       logins: [
-                               {
-                                       description: "Admin Role",
-                                       username: "TPAdmin",
-                                       password: "pa$$word"
-                               }
-                       ],
+                       description: "Admin Role Delivery Service actions",
+                       login: {
+                               username: "TPAdmin",
+                               password: "pa$$word"
+                       },
                        add: [
                                {
                                        description: "create ANY_MAP delivery 
service",
-                                       Name: "tpdservice1",
-                                       Tenant: "tenantSame",
-                                       Type: "ANY_MAP",
+                                       name: "tpdservice1",
+                                       tenant: "tenantSame",
+                                       type: "ANY_MAP",
                                        validationMessage: "Delivery Service 
creation was successful"
                                },
                                {
                                        description: "create DNS delivery 
service",
-                                       Name: "tpdservice2",
-                                       Tenant: "tenantSame",
-                                       Type: "DNS",
+                                       name: "tpdservice2",
+                                       tenant: "tenantSame",
+                                       type: "DNS",
                                        validationMessage: "Delivery Service 
creation was successful"
                                },
                                {
                                        description: "create STEERING delivery 
service",
-                                       Name: "tpdservice3",
-                                       Tenant: "tenantSame",
-                                       Type: "STEERING",
+                                       name: "tpdservice3",
+                                       tenant: "tenantSame",
+                                       type: "STEERING",
                                        validationMessage: "Delivery Service 
creation was successful"
                                }
                        ],
                        update: [
                                {
-                                       description: "update delivery service 
display name",
-                                       Name: "tpdservice1",
-                                       NewName: "TPServiceNew1",
+                                       name: "tpdservice1",
+                                       newName: "TPServiceNew1",
                                        validationMessage: "Delivery Service 
update was successful"
                                }
                        ],
-                       assignserver: [
+                       assignServer: [
                                {
-                                       description: "assign server to delivery 
service",
-                                       ServerName: "DSTest",
-                                       DSName: "TPServiceNew1",
+                                       serverHostname: "DSTest",
+                                       xmlID: "TPServiceNew1",
                                        validationMessage: "server assignments 
complete"
                                }
                        ],
-                       assignrequiredcapabilities: [
+                       assignRequiredCapabilities: [
                                {
-                                       description: "assign required 
capabilities to delivery service",
-                                       RCName: "DSTestCap",
-                                       DSName: "tpdservice2",
+                                       rcName: "DSTestCap",
+                                       xmlID: "tpdservice2",
                                        validationMessage: 
"deliveryservice.RequiredCapability was created."
                                }
                        ],
                        remove: [
                                {
-                                       description: "delete a delivery 
service",
-                                       Name: "tpdservice1",
+                                       name: "tpdservice1",
                                        validationMessage: "ds was deleted."
                                },
                                {
-                                       description: "delete a delivery 
service",
-                                       Name: "tpdservice2",
+                                       name: "tpdservice2",
                                        validationMessage: "ds was deleted."
                                },
                                {
-                                       description: "delete a delivery 
service",
-                                       Name: "tpdservice3",
+                                       name: "tpdservice3",
                                        validationMessage: "ds was deleted."
                                }
                        ]
                },
                {
-                       logins: [
-                               {
-                                       description: "Read Only Role",
-                                       username: "TPReadOnly",
-                                       password: "pa$$word"
-                               }
-                       ],
+                       description: "Read Only Role Delivery Service actions",
+                       login: {
+                               username: "TPReadOnly",
+                               password: "pa$$word"
+                       },
                        add: [
                                {
                                        description: "create ANY_MAP delivery 
service",
-                                       Name: "tpdservice1",
-                                       Type: "ANY_MAP",
-                                       Tenant: "tenantSame",
+                                       name: "tpdservice1",
+                                       type: "ANY_MAP",
+                                       tenant: "tenantSame",
                                        validationMessage: "missing required 
Permissions: DELIVERY-SERVICE:CREATE"
                                }
                        ],
                        update: [
                                {
-                                       description: "update delivery service 
display name",
-                                       Name: "dstestro1",
-                                       NewName: "TPServiceNew1",
+                                       name: "dstestro1",
+                                       newName: "TPServiceNew1",
                                        validationMessage: "missing required 
Permissions: DELIVERY-SERVICE:UPDATE"
                                }
                        ],
-                       assignserver: [
+                       assignServer: [
                                {
-                                       description: "assign server to delivery 
service",
-                                       ServerName: "DSTest",
-                                       DSName: "dstestro1",
+                                       serverHostname: "DSTest",
+                                       xmlID: "dstestro1",
                                        validationMessage: "missing required 
Permissions: SERVER:UPDATE, DELIVERY-SERVICE:UPDATE"
                                }
                        ],
-                       assignrequiredcapabilities: [
+                       assignRequiredCapabilities: [
                                {
-                                       description: "assign required 
capabilities to delivery service",
-                                       RCName: "DSTestCap",
-                                       DSName: "dstestro1",
+                                       rcName: "DSTestCap",
+                                       xmlID: "dstestro1",
                                        validationMessage: "missing required 
Permissions: DELIVERY-SERVICE:UPDATE"
                                }
                        ],
                        remove: [
                                {
-                                       description: "delete a delivery 
service",
-                                       Name: "dstestro1",
+                                       name: "dstestro1",
                                        validationMessage: "missing required 
Permissions: DELIVERY-SERVICE:DELETE"
                                }
                        ]
                },
                {
-                       logins: [
-                               {
-                                       description: "Operation Role",
-                                       username: "TPOperator",
-                                       password: "pa$$word"
-                               }
-                       ],
+                       description: "Operation Role Delivery Service actions",
+                       login: {
+                               username: "TPOperator",
+                               password: "pa$$word"
+                       },
                        add: [
                                {
                                        description: "create ANY_MAP delivery 
service",
-                                       Name: "optpdservice1",
-                                       Tenant: "tenantSame",
-                                       Type: "ANY_MAP",
+                                       name: "optpdservice1",
+                                       tenant: "tenantSame",
+                                       type: "ANY_MAP",
                                        validationMessage: "Delivery Service 
creation was successful"
                                },
                                {
                                        description: "create DNS delivery 
service",
-                                       Name: "optpdservice2",
-                                       Tenant: "tenantSame",
-                                       Type: "DNS",
+                                       name: "optpdservice2",
+                                       tenant: "tenantSame",
+                                       type: "DNS",
                                        validationMessage: "Delivery Service 
creation was successful"
                                },
                                {
                                        description: "create STEERING delivery 
service",
-                                       Name: "optpdservice3",
-                                       Tenant: "tenantSame",
-                                       Type: "STEERING",
+                                       name: "optpdservice3",
+                                       tenant: "tenantSame",
+                                       type: "STEERING",
                                        validationMessage: "Delivery Service 
creation was successful"
                                }
                        ],
                        update: [
                                {
-                                       description: "update delivery service 
display name",
-                                       Name: "optpdservice1",
-                                       NewName: "opTPServiceNew1",
+                                       name: "optpdservice1",
+                                       newName: "opTPServiceNew1",
                                        validationMessage: "Delivery Service 
update was successful"
                                }
                        ],
-                       assignserver: [
+                       assignServer: [
                                {
-                                       description: "assign server to delivery 
service",
-                                       ServerName: "DSTest",
-                                       DSName: "opTPServiceNew1",
+                                       serverHostname: "DSTest",
+                                       xmlID: "opTPServiceNew1",
                                        validationMessage: "server assignments 
complete"
                                }
                        ],
-                       assignrequiredcapabilities: [
+                       assignRequiredCapabilities: [
                                {
-                                       description: "assign required 
capabilities to delivery service",
-                                       RCName: "DSTestCap",
-                                       DSName: "optpdservice2",
+                                       rcName: "DSTestCap",
+                                       xmlID: "optpdservice2",
                                        validationMessage: 
"deliveryservice.RequiredCapability was created."
                                }
                        ],
                        remove: [
                                {
-                                       description: "delete a delivery 
service",
-                                       Name: "optpdservice1",
+                                       name: "optpdservice1",
                                        validationMessage: "ds was deleted."
                                },
                                {
-                                       description: "delete a delivery 
service",
-                                       Name: "optpdservice2",
+                                       name: "optpdservice2",
                                        validationMessage: "ds was deleted."
                                },
                                {
-                                       description: "delete a delivery 
service",
-                                       Name: "optpdservice3",
+                                       name: "optpdservice3",
                                        validationMessage: "ds was deleted."
                                }
                        ]
diff --git a/traffic_portal/test/integration/PageObjects/BasePage.po.ts 
b/traffic_portal/test/integration/PageObjects/BasePage.po.ts
index 17a5c0d298..4deff86844 100644
--- a/traffic_portal/test/integration/PageObjects/BasePage.po.ts
+++ b/traffic_portal/test/integration/PageObjects/BasePage.po.ts
@@ -19,8 +19,8 @@
 import { browser, element, by, ExpectedConditions } from 'protractor';
 /**
  * Class representing generic page.
- * Methods/properties for global elements should go here. 
- * 
+ * Methods/properties for global elements should go here.
+ *
  * @export
  * @class BasePage
  */
@@ -38,10 +38,10 @@ export class BasePage {
   private btnDeletePermanently = element(by.buttonText('Delete Permanently'));
   private btnCancel =  
element(by.className('close')).element(by.xpath("//span[text()='×']"));
   private btnUpdate = element(by.buttonText('Update'));
-  private btnSubmit = element(by.xpath("//button[text()='Submit']"));
+  private btnSubmit = element(by.buttonText("Submit"));
   private btnRegister = element(by.buttonText('Send Registration'));
-  private btnNo = element(by.xpath("//button[text()='No']"));
-  
+  private btnNo = element(by.buttonText("No"));
+
   async ClickNo(){
     await this.btnNo.click();
   }
@@ -52,7 +52,7 @@ export class BasePage {
     }else{
       return false;
     }
-    
+
   }
   public async ClickUpdate(): Promise<boolean>{
     if(await this.btnUpdate.isEnabled()){
diff --git 
a/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts 
b/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts
index 2fedf54cac..5c815e3686 100644
--- a/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts
+++ b/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts
@@ -17,203 +17,174 @@
  * under the License.
  */
 
-import { BasePage } from './BasePage.po';
+import { BasePage } from "./BasePage.po";
 import { randomize } from "../config";
-import { SideNavigationPage } from './SideNavigationPage.po';
-import {browser, by, element} from 'protractor';
-
-interface DeliveryServices {
-  Type: string;
-  Name: string;
-  Tenant: string;
-  validationMessage: string;
-}
-interface UpdateDeliveryService {
-  description: string;
-  Name: string;
-  NewName: string;
-  validationMessage: string;
-}
-interface DeleteDeliveryService {
-  Name: string;
-  validationMessage: string;
-}
-interface AssignServer {
-  DSName: string;
-  ServerName: string;
-  validationMessage: string;
-}
-interface AssignRC {
-  RCName: string;
-  DSName: string;
-  validationMessage: string;
-}
+import { SideNavigationPage } from "./SideNavigationPage.po";
+import { browser, by, element, ExpectedConditions } from "protractor";
+
+/**
+ * The DeliveryServicePage is a page object modelling of the Delivery Service
+ * editing/creation view. For simplicity"s sake, it also provides functionality
+ * that relates to the Delivery Services table view.
+ */
 export class DeliveryServicePage extends BasePage {
-  private btnCreateNewDeliveryServices = element(by.buttonText("Create 
Delivery Service"));
-  private mnuFormDropDown = element(by.name('selectFormDropdown'));
-  private btnSubmitFormDropDown = element(by.buttonText('Submit'));
-  private txtSearch = element(by.id("quickSearch"))
-  private txtConfirmName = element(by.name('confirmWithNameInput'));
-  private btnDelete = element(by.buttonText('Delete'));
-  private btnMore = element(by.name('moreBtn'));
-  private mnuManageRequiredServerCapabilities = element(by.linkText('Manage 
Required Server Capabilities'));
-  private btnAddRequiredServerCapabilities = 
element(by.name('addCapabilityBtn'));
-  private txtInputRC = element(by.name("selectFormDropdown"));
-  private mnuManageServers = element(by.buttonText('Manage Servers'));
-  private btnAssignServer = element(by.name("selectServersMenuItem"));
-  private txtXmlId = element(by.name('xmlId'));
-  private txtDisplayName = element(by.name('displayName'));
-  private selectActive = element(by.name('active'));
-  private selectType = element(by.id('type'));
-  private selectTenant = element(by.name('tenantId'));
-  private selectCDN = element(by.name('cdn'));
-  private txtOrgServerURL = element(by.name('orgServerFqdn'));
-  private txtProtocol = element(by.name('protocol'));
-  private txtRemapText = element(by.name('remapText'));
-  private btnCreateDeliveryServices = element(by.buttonText('Create'));
-  private randomize = randomize;
-
-  public async OpenDeliveryServicePage() {
-    const snp = new SideNavigationPage();
-    await snp.NavigateToDeliveryServicesPage();
-  }
-
-  public async OpenServicesMenu() {
-    const snp = new SideNavigationPage();
-    await snp.ClickServicesMenu();
-  }
-
-  public async CreateDeliveryService(deliveryservice: DeliveryServices): 
Promise<boolean> {
-    let result = false;
-    let type: string = deliveryservice.Type;
-    const basePage = new BasePage();
-    await this.btnMore.click();
-    await this.btnCreateNewDeliveryServices.click();
-    await this.mnuFormDropDown.sendKeys(type);
-    await this.btnSubmitFormDropDown.click();
-    switch (type) {
-      case "ANY_MAP": {
-        await this.txtXmlId.sendKeys(deliveryservice.Name + this.randomize);
-        await this.txtDisplayName.sendKeys(deliveryservice.Name + 
this.randomize);
-        await this.selectActive.sendKeys('Active')
-        await this.selectType.sendKeys('ANY_MAP')
-        await this.selectTenant.click();
-        await element(by.name(deliveryservice.Tenant + 
this.randomize)).click();
-        await this.selectCDN.sendKeys('dummycdn')
-        await this.txtRemapText.sendKeys('test')
-        break;
-      }
-      case "DNS": {
-        await this.txtXmlId.sendKeys(deliveryservice.Name + this.randomize);
-        await this.txtDisplayName.sendKeys(deliveryservice.Name + 
this.randomize);
-        await this.selectActive.sendKeys('Active')
-        await this.selectType.sendKeys('DNS')
-        await this.selectTenant.click();
-        await element(by.name(deliveryservice.Tenant + 
this.randomize)).click();
-        await this.selectCDN.sendKeys('dummycdn')
-        await this.txtOrgServerURL.sendKeys('http://origin.infra.ciab.test');
-        await this.txtProtocol.sendKeys('HTTP')
-        break;
-      }
-      case "HTTP": {
-        await this.txtXmlId.sendKeys(deliveryservice.Name + this.randomize);
-        await this.txtDisplayName.sendKeys(deliveryservice.Name + 
this.randomize);
-        await this.selectActive.sendKeys('Active')
-        await this.selectType.sendKeys('HTTP')
-        await this.selectTenant.click();
-        await element(by.name(deliveryservice.Tenant + 
this.randomize)).click();
-        await this.selectCDN.sendKeys('dummycdn')
-        await this.txtOrgServerURL.sendKeys('http://origin.infra.ciab.test');
-        await this.txtProtocol.sendKeys('HTTP')
-        break;
-      }
-      case "STEERING": {
-        await this.txtXmlId.sendKeys(deliveryservice.Name + this.randomize);
-        await this.txtDisplayName.sendKeys(deliveryservice.Name + 
this.randomize);
-        await this.selectActive.sendKeys('Active')
-        await this.selectType.sendKeys('STEERING')
-        await this.selectTenant.click();
-        await element(by.name(deliveryservice.Tenant + 
this.randomize)).click();
-        await this.selectCDN.sendKeys('dummycdn')
-        await this.txtProtocol.sendKeys('HTTP')
-        break;
-      }
-      default:
-        {
-          console.log('Wrong Type name');
-          break;
-        }
-    }
-    await this.btnCreateDeliveryServices.click();
-    result = await basePage.GetOutputMessage().then(value => value === 
deliveryservice.validationMessage);
-    return result;
-  }
-
-  public async SearchDeliveryService(nameDS: string): Promise<boolean> {
-    const name = nameDS + this.randomize;
-    await this.txtSearch.clear();
-    await this.txtSearch.sendKeys(name);
-    const result = await element(by.cssContainingText("span", 
name)).isPresent();
-    await element(by.cssContainingText("span", name)).click();
-    return !result;
-  }
-
-  public async UpdateDeliveryService(deliveryservice: UpdateDeliveryService): 
Promise<boolean | undefined> {
-    let result: boolean | undefined = false;
-    const basePage = new BasePage();
-    switch (deliveryservice.description) {
-      case "update delivery service display name":
-        await this.txtDisplayName.clear();
-        await this.txtDisplayName.sendKeys(deliveryservice.NewName + 
this.randomize);
-        await basePage.ClickUpdate();
-        break;
-      default:
-        result = undefined;
-    }
-    if (result = !undefined) {
-      result = await basePage.GetOutputMessage().then(value => value === 
deliveryservice.validationMessage);
-    }
-    return result;
-  }
-
-  public async DeleteDeliveryService(deliveryservice: DeleteDeliveryService): 
Promise<boolean> {
-    let result = false;
-    const basePage = new BasePage();
-    if (deliveryservice.validationMessage.includes("deleted")) {
-      deliveryservice.validationMessage = 
deliveryservice.validationMessage.replace(deliveryservice.Name, 
deliveryservice.Name + this.randomize);
-    }
-    await this.btnDelete.click();
-    await this.txtConfirmName.sendKeys(deliveryservice.Name + this.randomize);
-    await basePage.ClickDeletePermanently();
-    result = await basePage.GetOutputMessage().then(value => value === 
deliveryservice.validationMessage);
-    return result;
-  }
-
-  public async AssignServerToDeliveryService(deliveryservice: AssignServer): 
Promise<boolean>{
-    let result = false;
-    const basePage = new BasePage();
-    await this.btnMore.click();
-    await this.mnuManageServers.click();
-    await this.btnMore.click();
-    await this.btnAssignServer.click();
-    await browser.sleep(3000);
-    await element(by.cssContainingText(".ag-cell-value", 
deliveryservice.ServerName)).click();
-    await this.ClickSubmit();
-    result = await basePage.GetOutputMessage().then(value => value === 
deliveryservice.validationMessage);
-    return result;
-  }
-
-  public async AssignRequiredCapabilitiesToDS(deliveryservice: AssignRC): 
Promise<boolean>{
-    let result = false;
-    const basePage = new BasePage();
-    await this.btnMore.click();
-    await this.mnuManageRequiredServerCapabilities.click();
-    await this.btnAddRequiredServerCapabilities.click();
-    await this.txtInputRC.sendKeys(deliveryservice.RCName);
-    await this.ClickSubmit();
-    result = await basePage.GetOutputMessage().then(value => value === 
deliveryservice.validationMessage);
-    return result;
-  }
 
+       /** The search box in the DS table view. */
+       private readonly txtSearch = element(by.id("quickSearch"));
+
+       /** The "Display Name" text input in the editing/creation view(s). */
+       private readonly txtDisplayName = element(by.name("displayName"));
+       /** The "More" dropdown menu button in the editing/creation view(s). */
+       private readonly  btnMore = element(by.name("moreBtn"));
+
+       /**
+        * Navigates to the Delivery Services table view.
+        */
+       public async OpenDeliveryServicePage(): Promise<void> {
+               const snp = new SideNavigationPage();
+               return snp.NavigateToDeliveryServicesPage();
+       }
+
+       /**
+        * Toggles the open/close state of the "Services" sub-menu in the 
left-side
+        * navigation pane.
+        */
+       public async OpenServicesMenu(): Promise<void> {
+               const snp = new SideNavigationPage();
+               return snp.ClickServicesMenu();
+       }
+
+       /**
+        * Creates a new Delivery Service.
+        *
+        * @param deliveryservice Details for the Delivery Service to be 
created.
+        * @returns The text shown in the first Alert pane found after creation.
+        */
+       public async CreateDeliveryService(name: string, type: string, tenant: 
string): Promise<string> {
+               await this.btnMore.click();
+               await element(by.buttonText("Create Delivery Service")).click();
+               await element(by.name("selectFormDropdown")).sendKeys(type);
+               await element(by.buttonText("Submit")).click();
+
+               name += randomize;
+               tenant += randomize;
+
+               const ps = [];
+               switch (type) {
+                       case "ANY_MAP":
+                               
ps.push(element(by.name("remapText")).sendKeys("test"));
+                       break;
+
+                       case "DNS":
+                       case "HTTP":
+                               
ps.push(element(by.name("orgServerFqdn")).sendKeys("http://origin.infra.ciab.test";));
+                       case "STEERING":
+                               
ps.push(element(by.name("protocol")).sendKeys("HTTP"));
+                       break;
+
+                       default:
+                               throw new Error(`invalid Delivery Service 
routing type: ${type}`);
+               }
+               ps.push(
+                       element(by.name("xmlId")).sendKeys(name),
+                       this.txtDisplayName.sendKeys(name),
+                       element(by.name("active")).sendKeys("Active"),
+                       element(by.id("type")).sendKeys(type),
+                       element(by.name("tenantId")).click().then(() => 
element(by.name(tenant)).click()),
+                       element(by.name("cdn")).sendKeys("dummycdn")
+               );
+
+               await Promise.all(ps);
+               await element(by.buttonText("Create")).click();
+
+               return this.GetOutputMessage();
+       }
+
+       /**
+        * Searches the table for a Delivery Service in the table.
+        *
+        * (Note this neither checks nor enforces that the sought-after DS is
+        * actually found.)
+        *
+        * @param name The name for which to search.
+        */
+       public async SearchDeliveryService(name: string): Promise<void> {
+               name += randomize;
+
+               await this.txtSearch.clear();
+               await this.txtSearch.sendKeys(name);
+               const nameSpan = element(by.cssContainingText("span", name));
+               await nameSpan.click();
+       }
+
+       /**
+        * Changes a Delivery Service's Display Name to the provided value 
(after
+        * randomization).
+        *
+        * @param newName The new Display Name to be given to the Delivery 
Service.
+        * @returns The text shown in the first Alert pane found after 
attempting to
+        * submit the update.
+        */
+       public async UpdateDeliveryServiceDisplayName(newName: string): 
Promise<string> {
+               await this.txtDisplayName.clear();
+               await this.txtDisplayName.sendKeys(newName + randomize);
+               await this.ClickUpdate();
+               return this.GetOutputMessage();
+       }
+
+       /**
+        * Attempts to delete a Delivery Service.
+        *
+        * @param name The XMLID of the Delivery Service to be deleted.
+        * @returns The text shown in the first Alert pane found after 
attempting
+        * the deletion.
+        */
+       public async DeleteDeliveryService(name: string): Promise<string> {
+               name += randomize;
+               await element(by.buttonText("Delete")).click();
+               await element(by.name("confirmWithNameInput")).sendKeys(name);
+               await this.ClickDeletePermanently();
+               return this.GetOutputMessage();
+       }
+
+       /**
+        * Assigns the server with the given hostname to the Delivery Service. 
Note
+        * that the browser must already be on a Delivery Service edit view for 
this
+        * to work, as this method neither navigates to it nor back to the table
+        * view afterward!
+        *
+        * @param serverName The name of the server being assigned.
+        * @returns The text shown in the first Alert pane found after 
attempting
+        * the assignment.
+        */
+       public async AssignServerToDeliveryService(serverName: string): 
Promise<string>{
+               await this.btnMore.click();
+               await element(by.buttonText("Manage Servers")).click();
+               await this.btnMore.click();
+               await element(by.partialButtonText("Assign")).click();
+               const serverCell = 
element(by.cssContainingText(".ag-cell-value", serverName));
+               await 
browser.wait(ExpectedConditions.elementToBeClickable(serverCell), 3000);
+               await serverCell.click();
+               await this.ClickSubmit();
+               return this.GetOutputMessage();
+       }
 
+       /**
+        * Assigns the Capability with the given name as a requirement of the
+        * Delivery Service. Note that the browser must already be on a Delivery
+        * Service edit view for this to work, as this method neither navigates 
to
+        * it nor back to the table view afterward!
+        *
+        * @param name The name of the Capability to be required.
+        * @returns The text shown in the first Alert pane found after 
attempting
+        * the assignment.
+        */
+       public async AssignRequiredCapabilitiesToDS(name: string): 
Promise<string>{
+               await this.btnMore.click();
+               await element(by.linkText("Manage Required Server 
Capabilities")).click();
+               await element(by.name("addCapabilityBtn")).click();
+               await element(by.name("selectFormDropdown")).sendKeys(name);
+               await this.ClickSubmit();
+               return this.GetOutputMessage();
+       }
 }
diff --git a/traffic_portal/test/integration/specs/DeliveryServices.spec.ts 
b/traffic_portal/test/integration/specs/DeliveryServices.spec.ts
index 7c92742072..a7c8efb39b 100644
--- a/traffic_portal/test/integration/specs/DeliveryServices.spec.ts
+++ b/traffic_portal/test/integration/specs/DeliveryServices.spec.ts
@@ -28,72 +28,65 @@ const topNavigation = new TopNavigationPage();
 const loginPage = new LoginPage();
 const deliveryservicesPage = new DeliveryServicePage();
 
-describe('Setup API for delivery service test', function () {
-    it('Setup', async () => {
-        await api.UseAPI(deliveryservices.setup);
-    });
-});
+describe("Delivery Services", () => {
+       beforeAll(async () => {
+               await api.UseAPI(deliveryservices.setup);
+       });
+
+       afterAll(async () => {
+               await api.UseAPI(deliveryservices.cleanup);
+       });
+
+       for (const data of deliveryservices.tests) {
+               describe(`Traffic Portal - Delivery Service - 
${data.description}`, () =>{
+                       beforeAll(async () => {
+                               browser.get(browser.params.baseUrl);
+                               await loginPage.Login(data.login);
+                               expect(await 
loginPage.CheckUserName(data.login)).toBe(true);
+                               await deliveryservicesPage.OpenServicesMenu();
+                               await 
deliveryservicesPage.OpenDeliveryServicePage();
+                       });
+                       afterEach(async () => {
+                               await 
deliveryservicesPage.OpenDeliveryServicePage();
+                               expect((await 
browser.getCurrentUrl()).split("#").slice(-1).join().replace(/\/$/, 
"")).toBe("!/delivery-services");
+                       });
+                       afterAll(async () => {
+                               await deliveryservicesPage.OpenServicesMenu();
+                               expect(await topNavigation.Logout()).toBe(true);
+                       });
+
+                       for (const {description, name, type, tenant, 
validationMessage} of data.add) {
+                               it(description, async () => {
+                                       expect(await 
deliveryservicesPage.CreateDeliveryService(name, type, 
tenant)).toBe(validationMessage);
+                               });
+                       }
+                       for (const {name, newName, validationMessage} of 
data.update) {
+                               it("updates Delivery Service Display Name", 
async () => {
+                                       await 
deliveryservicesPage.SearchDeliveryService(name);
+                                       expect(await 
deliveryservicesPage.UpdateDeliveryServiceDisplayName(newName)).toBe(validationMessage);
+                               });
+                       }
 
-deliveryservices.tests.forEach(async deliveryservicesData => {
-    deliveryservicesData.logins.forEach(login =>{
-        describe(`Traffic Portal - Delivery Service - ${login.description}`, 
() =>{
-            it('can login', async () => {
-                browser.get(browser.params.baseUrl);
-                await loginPage.Login(login);
-                expect(await loginPage.CheckUserName(login)).toBe(true);
-            });
-            it('can open delivery service page', async () => {
-                await deliveryservicesPage.OpenServicesMenu();
-                await deliveryservicesPage.OpenDeliveryServicePage();
-            });
-            deliveryservicesData.add.forEach(add => {
-                it(add.description, async function () {
-                    expect(await 
deliveryservicesPage.CreateDeliveryService(add)).toBe(true);
-                    await deliveryservicesPage.OpenDeliveryServicePage();
-                });
-            });
-            deliveryservicesData.update.forEach(update => {
-                it(update.description, async function () {
-                    await 
deliveryservicesPage.SearchDeliveryService(update.Name);
-                    expect(await 
deliveryservicesPage.UpdateDeliveryService(update)).toBe(true);
-                    await deliveryservicesPage.OpenDeliveryServicePage();
-                });
-            })
-            deliveryservicesData.assignserver.forEach(assignserver => {
-                it(assignserver.description, async function(){
-                    await 
deliveryservicesPage.SearchDeliveryService(assignserver.DSName);
-                    expect(await 
deliveryservicesPage.AssignServerToDeliveryService(assignserver)).toBe(true);
-                    await deliveryservicesPage.OpenDeliveryServicePage();
-                }
+                       for (const {serverHostname, xmlID, validationMessage} 
of data.assignServer){
+                               it("assigns servers to a Delivery Service", 
async () => {
+                                       await 
deliveryservicesPage.SearchDeliveryService(xmlID);
+                                       expect(await 
deliveryservicesPage.AssignServerToDeliveryService(serverHostname)).toBe(validationMessage);
+                               });
+                       }
 
-                )
-            })
-            deliveryservicesData.assignrequiredcapabilities.forEach(assignrc 
=> {
-                it(assignrc.description, async function(){
-                    await 
deliveryservicesPage.SearchDeliveryService(assignrc.DSName);
-                    expect(await 
deliveryservicesPage.AssignRequiredCapabilitiesToDS(assignrc)).toBe(true);
-                    await deliveryservicesPage.OpenDeliveryServicePage();
-                })
-            })
-            deliveryservicesData.remove.forEach(remove => {
-                it(remove.description, async () => {
-                    await 
deliveryservicesPage.SearchDeliveryService(remove.Name);
-                    expect(await 
deliveryservicesPage.DeleteDeliveryService(remove)).toBe(true);
-                    await deliveryservicesPage.OpenDeliveryServicePage();
-                });
-            });
-            it('can close service menu tab', async () => {
-                await deliveryservicesPage.OpenServicesMenu();
-            });
-            it('can logout', async () => {
-                expect(await topNavigation.Logout()).toBe(true);
-            });
-        })
-    })
+                       for (const {rcName, validationMessage, xmlID} of 
data.assignRequiredCapabilities) {
+                               it("assign required capabilities to delivery 
service", async () => {
+                                       await 
deliveryservicesPage.SearchDeliveryService(xmlID);
+                                       expect(await 
deliveryservicesPage.AssignRequiredCapabilitiesToDS(rcName)).toBe(validationMessage);
+                               });
+                       }
 
-})
-describe('Clean up API for delivery service test', () => {
-    it('Cleanup', async () => {
-        await api.UseAPI(deliveryservices.cleanup);
-    });
+                       for (const {name, validationMessage} of data.remove) {
+                               it("deletes a Delivery Service", async () => {
+                                       await 
deliveryservicesPage.SearchDeliveryService(name);
+                                       expect(await 
deliveryservicesPage.DeleteDeliveryService(name)).toBe(validationMessage);
+                               });
+                       }
+               });
+       }
 });

Reply via email to