You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by sh...@apache.org on 2022/08/25 15:19:28 UTC

[trafficcontrol] branch master updated: Add the ability to toggle sensitive data fields to common grids (#6990)

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 <oc...@apache.org>
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);
+				});
+			}
+		});
+	}
 });