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/02 21:09:10 UTC

[trafficcontrol] branch master updated: Convert Delivery Services table to use "common" grid controller (#6989)

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 4060da154b Convert Delivery Services table to use "common" grid controller (#6989)
4060da154b is described below

commit 4060da154b38f93dd1ee531ae00cd599908fea1f
Author: ocket8888 <oc...@apache.org>
AuthorDate: Tue Aug 2 15:09:04 2022 -0600

    Convert Delivery Services table to use "common" grid controller (#6989)
    
    * Fix grids using an empty string for table titles
    
    * Convert Delivery Services table to use common grid controller
    
    * Fix tests looking for a link that's now a button
---
 .../modules/table/agGrid/CommonGridController.js   |    2 -
 .../src/common/modules/table/agGrid/grid.tpl.html  |    4 +-
 .../common/modules/table/cdns/table.cdns.tpl.html  |    2 +-
 .../TableDeliveryServicesController.js             | 1525 +++++++++-----------
 .../table.deliveryServices.tpl.html                |  111 +-
 .../modules/table/profiles/table.profiles.tpl.html |    2 +-
 .../modules/private/cacheStats/cacheStats.tpl.html |    2 +-
 .../PageObjects/DeliveryServicePage.po.ts          |    2 +-
 .../PageObjects/DeliveryServiceRequestPage.po.ts   |    3 +-
 9 files changed, 692 insertions(+), 961 deletions(-)

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 aaf1868f14..c8571f4f7d 100644
--- a/traffic_portal/app/src/common/modules/table/agGrid/CommonGridController.js
+++ b/traffic_portal/app/src/common/modules/table/agGrid/CommonGridController.js
@@ -147,8 +147,6 @@ let CommonGridController = function ($scope, $document, $state, userModel, dateU
 
     // Bound Variables
     /** @type string */
-    this.title = "";
-    /** @type string */
     this.tableName = "";
     /** @type CGC.GridSettings */
     this.options = {};
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 c172a584e8..9b8bda766b 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
@@ -26,8 +26,8 @@ under the License.
             </li>
         </ol>
         <ol class="breadcrumb pull-left" ng-if="$ctrl.breadCrumbs === undefined || $ctrl.breadCrumbs.length < 1">
-            <li class="active" ng-if="$ctrl.title">
-                {{ $ctrl.title }}
+            <li class="active" ng-if="$ctrl.tableTitle">
+                {{ $ctrl.tableTitle }}
             </li>
         </ol>
         <button ng-if="$ctrl.titleButton !== undefined" type="button" class="btn btn-link"
diff --git a/traffic_portal/app/src/common/modules/table/cdns/table.cdns.tpl.html b/traffic_portal/app/src/common/modules/table/cdns/table.cdns.tpl.html
index c6c6c8665d..c17b8e5aa2 100644
--- a/traffic_portal/app/src/common/modules/table/cdns/table.cdns.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/cdns/table.cdns.tpl.html
@@ -19,7 +19,7 @@ under the License.
 
 <div class="x_panel">
 	<common-grid-controller
-		title="CDNs"
+		table-title="CDNs"
 		table-name="cdns"
 		options="gridOptions"
 		data="cdns"
diff --git a/traffic_portal/app/src/common/modules/table/deliveryServices/TableDeliveryServicesController.js b/traffic_portal/app/src/common/modules/table/deliveryServices/TableDeliveryServicesController.js
index 35c9b9e58e..5ef3e1b25c 100644
--- a/traffic_portal/app/src/common/modules/table/deliveryServices/TableDeliveryServicesController.js
+++ b/traffic_portal/app/src/common/modules/table/deliveryServices/TableDeliveryServicesController.js
@@ -17,856 +17,681 @@
  * under the License.
  */
 
-var TableDeliveryServicesController = function(tableName, deliveryServices, filter, $anchorScroll, $document, $scope, $state, $location, $uibModal, deliveryServiceService, deliveryServiceRequestService, dateUtils, deliveryServiceUtils, locationUtils, messageModel, propertiesModel, userModel) {
-
-    /**
-     * Gets value to display a default tooltip.
-     */
-    function defaultTooltip(params) {
-        return params.value;
-    }
-
-    /**
-     * Formats the contents of a 'lastUpdated' column cell as "relative to now".
-     */
-    function dateCellFormatter(params) {
-        return params.value ? dateUtils.getRelativeTime(params.value) : params.value;
-    }
-
-    /** The columns of the ag-grid table */
-    const columns = [
-        {
-            headerName: "Active",
-            field: "active",
-            hide: false
-        },
-        {
-            headerName: "Anonymous Blocking",
-            field: "anonymousBlockingEnabled",
-            hide: true
-        },
-        {
-            headerName: "CDN",
-            field: "cdnName",
-            hide: false
-        },
-        {
-            headerName: "Check Path",
-            field: "checkPath",
-            hide: true
-        },
-        {
-            headerName: "Consistent Hash Query Params",
-            field: "consistentHashQueryParams",
-            hide: true,
-            valueFormatter: function(params) {
-                return params.data.consistentHashQueryParams.join(', ');
-            },
-            tooltipValueGetter: function(params) {
-                return params.data.consistentHashQueryParams.join(', ');
-            }
-        },
-        {
-            headerName: "Consistent Hash Regex",
-            field: "consistentHashRegex",
-            hide: true
-        },
-        {
-            headerName: "Deep Caching Type",
-            field: "deepCachingType",
-            hide: true
-        },
-        {
-            headerName: "Display Name",
-            field: "displayName",
-            hide: false
-        },
-        {
-            headerName: "DNS Bypass CNAME",
-            field: "dnsBypassCname",
-            hide: true
-        },
-        {
-            headerName: "DNS Bypass IP",
-            field: "dnsBypassIp",
-            hide: true
-        },
-        {
-            headerName: "DNS Bypass IPv6",
-            field: "dnsBypassIp6",
-            hide: true
-        },
-        {
-            headerName: "DNS Bypass TTL",
-            field: "dnsBypassTtl",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "DNS TTL",
-            field: "ccrDnsTtl",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "DSCP",
-            field: "dscp",
-            hide: false,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "ECS Enabled",
-            field: "ecsEnabled",
-            hide: true
-        },
-        {
-            headerName: "Edge Header Rewrite Rules",
-            field: "edgeHeaderRewrite",
-            hide: true
-        },
-        {
-            headerName: "First Header Rewrite Rules",
-            field: "firstHeaderRewrite",
-            hide: true
-        },
-        {
-            headerName: "FQ Pacing Rate",
-            field: "fqPacingRate",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "Geo Limit",
-            field: "geoLimit",
-            hide: true,
-            valueFormatter: function(params) {
-                return deliveryServiceUtils.geoLimits[params.data.geoLimit];
-            },
-            tooltipValueGetter: function(params) {
-                return deliveryServiceUtils.geoLimits[params.data.geoLimit];
-            }
-        },
-        {
-            headerName: "Geo Limit Countries",
-            field: "geoLimitCountries",
-            hide: true
-        },
-        {
-            headerName: "Geo Limit Redirect URL",
-            field: "geoLimitRedirectURL",
-            hide: true
-        },
-        {
-            headerName: "Geolocation Provider",
-            field: "geoProvider",
-            hide: true,
-            valueFormatter: function(params) {
-                return deliveryServiceUtils.geoProviders[params.data.geoProvider];
-            },
-            tooltipValueGetter: function(params) {
-                return deliveryServiceUtils.geoProviders[params.data.geoProvider];
-            }
-        },
-        {
-            headerName: "Geo Miss Latitude",
-            field: "missLat",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "Geo Miss Longitude",
-            field: "missLong",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "Global Max Mbps",
-            field: "globalMaxMbps",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "Global Max TPS",
-            field: "globalMaxTps",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "HTTP Bypass FQDN",
-            field: "httpBypassFqdn",
-            hide: true
-        },
-        {
-            headerName: "ID",
-            field: "id",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "Info URL",
-            field: "infoUrl",
-            hide: true
-        },
-        {
-            headerName: "Initial Dispersion",
-            field: "initialDispersion",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "Inner Header Rewrite Rules",
-            field: "innerHeaderRewrite",
-            hide: true
-        },
-        {
-            headerName: "IPv6 Routing",
-            field: "ipv6RoutingEnabled",
-            hide: true
-        },
-        {
-            headerName: "Last Header Rewrite Rules",
-            field: "lastHeaderRewrite",
-            hide: true
-        },
-        {
-            headerName: "Last Updated",
-            field: "lastUpdated",
-            hide: true,
-            filter: "agDateColumnFilter",
-            valueFormatter: dateCellFormatter
-        },
-        {
-            headerName: "Long Desc",
-            field: "longDesc",
-            hide: true
-        },
-        {
-            headerName: "Max DNS Answers",
-            field: "maxDnsAnswers",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "Max Origin Connections",
-            field: "maxOriginConnections",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "Max Request Header Bytes",
-            field: "maxRequestHeaderBytes",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "Mid Header Rewrite Rules",
-            field: "midHeaderRewrite",
-            hide: true
-        },
-        {
-            headerName: "Multi-Site Origin",
-            field: "multiSiteOrigin",
-            hide: true
-        },
-        {
-            headerName: "Origin Shield",
-            field: "originShield",
-            hide: true
-        },
-        {
-            headerName: "Origin FQDN",
-            field: "orgServerFqdn",
-            hide: false
-        },
-        {
-            headerName: "Profile",
-            field: "profileName",
-            hide: true
-        },
-        {
-            headerName: "Protocol",
-            field: "protocol",
-            hide: false,
-            valueFormatter: function(params) {
-                return deliveryServiceUtils.protocols[params.data.protocol];
-            },
-            tooltipValueGetter: function(params) {
-                return deliveryServiceUtils.protocols[params.data.protocol];
-            }
-        },
-        {
-            headerName: "Qstring Handling",
-            field: "qstringIgnore",
-            hide: true,
-            valueFormatter: function(params) {
-                return deliveryServiceUtils.qstrings[params.data.qstringIgnore];
-            },
-            tooltipValueGetter: function(params) {
-                return deliveryServiceUtils.qstrings[params.data.qstringIgnore];
-            }
-        },
-        {
-            headerName: "Range Request Handling",
-            field: "rangeRequestHandling",
-            hide: true,
-            valueFormatter: function(params) {
-                return deliveryServiceUtils.rrhs[params.data.rangeRequestHandling];
-            },
-            tooltipValueGetter: function(params) {
-                return deliveryServiceUtils.rrhs[params.data.rangeRequestHandling];
-            }
-        },
-        {
-            headerName: "Regex Remap Expression",
-            field: "regexRemap",
-            hide: true
-        },
-        {
-            headerName: "Regional Geoblocking",
-            field: "regionalGeoBlocking",
-            hide: true
-        },
-        {
-            headerName: "Raw Remap Text",
-            field: "remapText",
-            hide: true
-        },
-        {
-            headerName: "Routing Name",
-            field: "routingName",
-            hide: true
-        },
-        {
-            headerName: "Service Category",
-            field: "serviceCategory",
-            hide: true
-        },
-        {
-            headerName: "Signed",
-            field: "signed",
-            hide: true
-        },
-        {
-            headerName: "Signing Algorithm",
-            field: "signingAlgorithm",
-            hide: true
-        },
-        {
-            headerName: "Range Slice Block Size",
-            field: "rangeSliceBlockSize",
-            hide: true,
-            filter: "agNumberColumnFilter"
-        },
-        {
-            headerName: "Tenant",
-            field: "tenant",
-            hide: false
-        },
-        {
-            headerName: "Topology",
-            field: "topology",
-            hide: false
-        },
-        {
-            headerName: "TR Request Headers",
-            field: "trRequestHeaders",
-            hide: true
-        },
-        {
-            headerName: "TR Response Headers",
-            field: "trResponseHeaders",
-            hide: true
-        },
-        {
-            headerName: "Type",
-            field: "type",
-            hide: false
-        },
-        {
-            headerName: "XML ID (Key)",
-            field: "xmlId",
-            hide: false
-        }
-    ];
-
-    let dsRequestsEnabled = propertiesModel.properties.dsRequests.enabled;
-
-    let showCustomCharts = propertiesModel.properties.deliveryServices.charts.customLink.show;
-
-    var createDeliveryService = function(typeName) {
-        var path = '/delivery-services/new?dsType=' + typeName;
-        locationUtils.navigateToPath(path);
-    };
-
-    $scope.clone = function(ds, event) {
-        event.stopPropagation();
-        var params = {
-            title: 'Clone Delivery Service: ' + ds.xmlId,
-            message: "Please select a content routing category for the clone"
-        };
-        var modalInstance = $uibModal.open({
-            templateUrl: 'common/modules/dialog/select/dialog.select.tpl.html',
-            controller: 'DialogSelectController',
-            size: 'md',
-            resolve: {
-                params: function () {
-                    return params;
-                },
-                collection: function() {
-                    // the following represent the 4 categories of delivery services
-                    // the ids are arbitrary but the dialog.select dropdown needs them
-                    return [
-                        { id: 1, name: 'ANY_MAP' },
-                        { id: 2, name: 'DNS' },
-                        { id: 3, name: 'HTTP' },
-                        { id: 4, name: 'STEERING' }
-                    ];
-                }
-            }
-        });
-        modalInstance.result.then(function(type) {
-            locationUtils.navigateToPath('/delivery-services/' + ds.id + '/clone?dsType=' + type.name);
-        });
-    };
-
-    $scope.confirmDelete = function(deliveryService, event) {
-        event.stopPropagation();
-        var params = {
-            title: 'Delete Delivery Service: ' + deliveryService.xmlId,
-            key: deliveryService.xmlId
-        };
-        var modalInstance = $uibModal.open({
-            templateUrl: 'common/modules/dialog/delete/dialog.delete.tpl.html',
-            controller: 'DialogDeleteController',
-            size: 'md',
-            resolve: {
-                params: function () {
-                    return params;
-                }
-            }
-        });
-        modalInstance.result.then(function() {
-            if (dsRequestsEnabled) {
-                createDeliveryServiceDeleteRequest(deliveryService);
-            } else {
-                deliveryServiceService.deleteDeliveryService(deliveryService)
-                    .then(
-                        function() {
-                            messageModel.setMessages([ { level: 'success', text: 'Delivery service [ ' + deliveryService.xmlId + ' ] deleted' } ], false);
-                            $scope.refresh();                        },
-                        function(fault) {
-                            $anchorScroll(); // scrolls window to top
-                            messageModel.setMessages(fault.data.alerts, false);
-                        }
-                    );
-            }
-        }, function () {
-            // do nothing
-        });
-    };
-
-    var createDeliveryServiceDeleteRequest = function(deliveryService) {
-        var params = {
-            title: "Delivery Service Delete Request",
-            message: 'All delivery service deletions must be reviewed.'
-        };
-        var modalInstance = $uibModal.open({
-            templateUrl: 'common/modules/dialog/deliveryServiceRequest/dialog.deliveryServiceRequest.tpl.html',
-            controller: 'DialogDeliveryServiceRequestController',
-            size: 'md',
-            resolve: {
-                params: function () {
-                    return params;
-                },
-                statuses: function() {
-                    var statuses = [
-                        { id: $scope.DRAFT, name: 'Save Request as Draft' },
-                        { id: $scope.SUBMITTED, name: 'Submit Request for Review and Deployment' }
-                    ];
-                    if (userModel.user.role == propertiesModel.properties.dsRequests.overrideRole) {
-                        statuses.push({ id: $scope.COMPLETE, name: 'Fulfill Request Immediately' });
-                    }
-                    return statuses;
-                }
-            }
-        });
-        modalInstance.result.then(function(options) {
-            var status = 'draft';
-            if (options.status.id == $scope.SUBMITTED || options.status.id == $scope.COMPLETE) {
-                status = 'submitted';
-            };
-
-            var dsRequest = {
-                changeType: 'delete',
-                status: status,
-                original: deliveryService
-            };
-
-            // if the user chooses to complete/fulfill the delete request immediately, the ds will be deleted and behind the
-            // scenes a delivery service request will be created and marked as complete
-            if (options.status.id == $scope.COMPLETE) {
-                // first delete the ds
-                deliveryServiceService.deleteDeliveryService(deliveryService)
-                    .then(
-                        function() {
-                            // then create the ds request
-                            deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest).
-                            then(
-                                function(response) {
-                                    var comment = {
-                                        deliveryServiceRequestId: response.id,
-                                        value: options.comment
-                                    };
-                                    // then create the ds request comment
-                                    deliveryServiceRequestService.createDeliveryServiceRequestComment(comment).
-                                    then(
-                                        function() {
-                                            var promises = [];
-                                            // assign the ds request
-                                            promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(response.id, userModel.user.username));
-                                            // set the status to 'complete'
-                                            promises.push(deliveryServiceRequestService.updateDeliveryServiceRequestStatus(response.id, 'complete'));
-                                            // and finally refresh the delivery services table
-                                            messageModel.setMessages([ { level: 'success', text: 'Delivery service [ ' + deliveryService.xmlId + ' ] deleted' } ], false);
-                                            $scope.refresh();
-                                        }
-                                    );
-                                }
-                            );
-                        },
-                        function(fault) {
-                            $anchorScroll(); // scrolls window to top
-                            messageModel.setMessages(fault.data.alerts, false);
-                        }
-                    );
-            } else {
-                deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest).
-                    then(
-                        function(response) {
-                            var comment = {
-                                deliveryServiceRequestId: response.id,
-                                value: options.comment
-                            };
-                            deliveryServiceRequestService.createDeliveryServiceRequestComment(comment).
-                                then(
-                                    function() {
-                                        const xmlId = (dsRequest.requested) ? dsRequest.requested.xmlId : dsRequest.original.xmlId;
-                                        messageModel.setMessages([ { level: 'success', text: 'Created request to ' + dsRequest.changeType + ' the ' + xmlId + ' delivery service' } ], true);
-                                        locationUtils.navigateToPath('/delivery-service-requests');
-                                    }
-                                );
-                        }
-                    );
-            }
-        });
-    };
-
-    /** All of the delivery services - lastUpdated fields converted to actual Dates */
-    $scope.deliveryServices = deliveryServices.map(
-        function(x) {
-            x.lastUpdated = x.lastUpdated ? new Date(x.lastUpdated.replace("+00", "Z")) : x.lastUpdated;
-        });
-
-    /** The currently selected server - at the moment only used by the context menu */
-    $scope.deliveryService = {
-        xmlId: "",
-        id: -1
-    };
-
-    $scope.quickSearch = '';
-
-    $scope.pageSize = 100;
-
-    $scope.mouseDownSelectionText = "";
-
-    $scope.navigateToPath = locationUtils.navigateToPath;
-
-    $scope.DRAFT = 0;
-    $scope.SUBMITTED = 1;
-    $scope.REJECTED = 2;
-    $scope.PENDING = 3;
-    $scope.COMPLETE = 4;
-
-    $scope.viewCharts = function(ds, $event) {
-        $event.stopPropagation();
-        if (showCustomCharts) {
-            deliveryServiceUtils.openCharts(ds);
-        } else {
-            locationUtils.navigateToPath('/delivery-services/' + ds.id + '/charts?dsType=' + ds.type);
-        }
-    };
-
-    $scope.refresh = function() {
-        $state.reload(); // reloads all the resolves for the view
-    };
-
-    $scope.selectDSType = function() {
-        var params = {
-            title: 'Create Delivery Service',
-            message: "Please select a content routing category"
-        };
-        var modalInstance = $uibModal.open({
-            templateUrl: 'common/modules/dialog/select/dialog.select.tpl.html',
-            controller: 'DialogSelectController',
-            size: 'md',
-            resolve: {
-                params: function () {
-                    return params;
-                },
-                collection: function() {
-                    // the following represent the 4 categories of delivery services
-                    // the ids are arbitrary but the dialog.select dropdown needs them
-                    return [
-                        { id: 1, name: 'ANY_MAP' },
-                        { id: 2, name: 'DNS' },
-                        { id: 3, name: 'HTTP' },
-                        { id: 4, name: 'STEERING' }
-                    ];
-                }
-            }
-        });
-        modalInstance.result.then(function(type) {
-            createDeliveryService(type.name);
-        }, function () {
-            // do nothing
-        });
-    };
-
-    $scope.compareDSs = function() {
-        var params = {
-            title: 'Compare Delivery Services',
-            message: "Please select 2 delivery services to compare",
-            label: "xmlId"
-        };
-        var modalInstance = $uibModal.open({
-            templateUrl: 'common/modules/dialog/compare/dialog.compare.tpl.html',
-            controller: 'DialogCompareController',
-            size: 'md',
-            resolve: {
-                params: function () {
-                    return params;
-                },
-                collection: function(deliveryServiceService) {
-                    return deliveryServiceService.getDeliveryServices();
-                }
-            }
-        });
-        modalInstance.result.then(function(dss) {
-            $location.path($location.path() + '/compare/' + dss[0].id + '/' + dss[1].id);
-        }, function () {
-            // do nothing
-        });
-    };
-
-    /** Toggles the visibility of a column that has the ID provided as 'col'. */
-    $scope.toggleVisibility = function(col) {
-        const visible = $scope.gridOptions.columnApi.getColumn(col).isVisible();
-        $scope.gridOptions.columnApi.setColumnVisible(col, !visible);
-    };
-
-    /** Options, configuration, data and callbacks for the ag-grid table. */
-    $scope.gridOptions = {
-        columnDefs: columns,
-        enableCellTextSelection:true,
-        suppressMenuHide: true,
-        multiSortKey: 'ctrl',
-        alwaysShowVerticalScroll: true,
-        defaultColDef: {
-            filter: true,
-            sortable: true,
-            resizable: true,
-            tooltipValueGetter: defaultTooltip
-        },
-        rowData: deliveryServices,
-        pagination: true,
-        paginationPageSize: $scope.pageSize,
-        rowBuffer: 0,
-        tooltipShowDelay: 500,
-        allowContextMenuWithControlKey: true,
-        preventDefaultOnContextMenu: true,
-        colResizeDefault: "shift",
-        onColumnVisible: function(params) {
-            if (params.visible){
-                return;
-            }
-            const filterModel = $scope.gridOptions.api.getFilterModel();
-            for (let column of params.columns) {
-                if (column.filterActive) {
-                    if (column.colId in filterModel) {
-                        delete filterModel[column.colId];
-                        $scope.gridOptions.api.setFilterModel(filterModel);
-                    }
-                }
-            }
-        },
-        onCellContextMenu: function(params) {
-            $scope.showMenu = true;
-            $scope.menuStyle.left = String(params.event.clientX) + "px";
-            $scope.menuStyle.top = String(params.event.clientY) + "px";
-            $scope.menuStyle.bottom = "unset";
-            $scope.menuStyle.right = "unset";
-            $scope.$apply();
-            const boundingRect = document.getElementById("context-menu").getBoundingClientRect();
-
-            if (boundingRect.bottom > window.innerHeight){
-                $scope.menuStyle.bottom = String(window.innerHeight - params.event.clientY) + "px";
-                $scope.menuStyle.top = "unset";
-            }
-            if (boundingRect.right > window.innerWidth) {
-                $scope.menuStyle.right = String(window.innerWidth - params.event.clientX) + "px";
-                $scope.menuStyle.left = "unset";
-            }
-            $scope.deliveryService = params.data;
-            $scope.$apply();
-        },
-        onCellMouseDown: function() {
-            $scope.mouseDownSelectionText = window.getSelection().toString();
-        },
-        onRowClicked: function(params) {
-            const selection = window.getSelection().toString();
-            if(selection === "" || selection === $scope.mouseDownSelectionText) {
-                locationUtils.navigateToPath('/delivery-services/' + params.data.id + '?dsType=' + params.data.type);
-                // Event is outside the digest cycle, so we need to trigger one.
-                $scope.$apply();
-            }
-            $scope.mouseDownSelectionText = "";
-        },
-        onColumnResized: function(params) {
-            localStorage.setItem(tableName + "_table_columns", JSON.stringify($scope.gridOptions.columnApi.getColumnState()));
-        },
-        onFirstDataRendered: function(event) {
-            try {
-                const filterState = JSON.parse(localStorage.getItem(tableName + "_table_filters")) || {};
-                // apply any filter provided to the controller
-                Object.assign(filterState, filter);
-                $scope.gridOptions.api.setFilterModel(filterState);
-            } catch (e) {
-                console.error("Failure to load stored filter state:", e);
-            }
-
-            $scope.gridOptions.api.addEventListener("filterChanged", function() {
-                localStorage.setItem(tableName + "_table_filters", JSON.stringify($scope.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 (!$scope.gridOptions.columnApi.setColumnState(colstates)) {
-                        console.error("Failed to load stored column state: one or more columns not found");
-                    }
-                } else {
-                    $scope.gridOptions.api.sizeColumnsToFit();
-                }
-            } catch (e) {
-                console.error("Failure to retrieve required column info from localStorage (key=" + tableName + "_table_columns):", e);
-            }
-
-            try {
-                const sortState = JSON.parse(localStorage.getItem(tableName + "_table_sort"));
-                $scope.gridOptions.api.setSortModel(sortState);
-            } catch (e) {
-                console.error("Failure to load stored sort state:", e);
-            }
-
-            try {
-                $scope.quickSearch = localStorage.getItem(tableName + "_quick_search");
-                $scope.gridOptions.api.setQuickFilter($scope.quickSearch);
-            } catch (e) {
-                console.error("Failure to load stored quick search:", e);
-            }
-
-            try {
-                const ps = localStorage.getItem(tableName + "_page_size");
-                if (ps && ps > 0) {
-                    $scope.pageSize = Number(ps);
-                    $scope.gridOptions.api.paginationSetPageSize($scope.pageSize);
-                }
-            } catch (e) {
-                console.error("Failure to load stored page size:", e);
-            }
-            
-            try {
-                const page = parseInt(localStorage.getItem(tableName + "_table_page"));
-                const totalPages = $scope.gridOptions.api.paginationGetTotalPages();
-                if (page !== undefined && page > 0 && page <= totalPages-1) {
-                    $scope.gridOptions.api.paginationGoToPage(page);
-                }
-            } catch (e) {
-                console.error("Failed to load stored page number:", e);
-            }
-
-            $scope.gridOptions.api.addEventListener("paginationChanged", function() {
-                localStorage.setItem(tableName + "_table_page", $scope.gridOptions.api.paginationGetCurrentPage());
-            });
-
-            $scope.gridOptions.api.addEventListener("sortChanged", function() {
-                localStorage.setItem(tableName + "_table_sort", JSON.stringify($scope.gridOptions.api.getSortModel()));
-            });
-
-            $scope.gridOptions.api.addEventListener("columnMoved", function() {
-                localStorage.setItem(tableName + "_table_columns", JSON.stringify($scope.gridOptions.columnApi.getColumnState()));
-            });
-
-            $scope.gridOptions.api.addEventListener("columnVisible", function() {
-                $scope.gridOptions.api.sizeColumnsToFit();
-                try {
-                    const colStates = $scope.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 is used to position the context menu under the cursor. */
-    $scope.menuStyle = {
-        left: 0,
-        top: 0,
-    };
-
-    /** Controls whether or not the context menu is visible. */
-    $scope.showMenu = false;
-
-    /** Downloads the table as a CSV */
-    $scope.exportCSV = function() {
-        const params = {
-            allColumns: true,
-            fileName: "delivery-services.csv",
-        };
-        $scope.gridOptions.api.exportDataAsCsv(params);
-    };
-
-    $scope.onQuickSearchChanged = function() {
-        $scope.gridOptions.api.setQuickFilter($scope.quickSearch);
-        localStorage.setItem(tableName + "_quick_search", $scope.quickSearch);
-    };
-
-    $scope.onPageSizeChanged = function() {
-        const value = Number($scope.pageSize);
-        $scope.gridOptions.api.paginationSetPageSize(value);
-        localStorage.setItem(tableName + "_page_size", value);
-    };
-
-    $scope.clearTableFilters = function() {
-        // clear the quick search
-        $scope.quickSearch = '';
-        $scope.onQuickSearchChanged();
-        // clear any column filters
-        $scope.gridOptions.api.setFilterModel(null);
-    };
-
-    angular.element(document).ready(function () {
-        // clicks outside the context menu will hide it
-        $document.bind("click", function(e) {
-            $scope.showMenu = false;
-            e.stopPropagation();
-            $scope.$apply();
-        });
-    });
-
+function TableDeliveryServicesController(tableName, deliveryServices, $anchorScroll, $scope, $state, $location, $uibModal, deliveryServiceService, deliveryServiceRequestService, deliveryServiceUtils, locationUtils, messageModel, propertiesModel, userModel) {
+	$scope.tableName = tableName;
+
+	/** The columns of the ag-grid table */
+	$scope.columns = [
+		{
+			headerName: "Active",
+			field: "active",
+			hide: false
+		},
+		{
+			headerName: "Anonymous Blocking",
+			field: "anonymousBlockingEnabled",
+			hide: true
+		},
+		{
+			headerName: "CDN",
+			field: "cdnName",
+			hide: false
+		},
+		{
+			headerName: "Check Path",
+			field: "checkPath",
+			hide: true
+		},
+		{
+			headerName: "Consistent Hash Query Params",
+			field: "consistentHashQueryParams",
+			hide: true,
+			valueFormatter: params => params.data.consistentHashQueryParams.join(', '),
+			tooltipValueGetter: params => params.data.consistentHashQueryParams.join(', ')
+		},
+		{
+			headerName: "Consistent Hash Regex",
+			field: "consistentHashRegex",
+			hide: true
+		},
+		{
+			headerName: "Deep Caching Type",
+			field: "deepCachingType",
+			hide: true
+		},
+		{
+			headerName: "Display Name",
+			field: "displayName",
+			hide: false
+		},
+		{
+			headerName: "DNS Bypass CNAME",
+			field: "dnsBypassCname",
+			hide: true
+		},
+		{
+			headerName: "DNS Bypass IP",
+			field: "dnsBypassIp",
+			hide: true
+		},
+		{
+			headerName: "DNS Bypass IPv6",
+			field: "dnsBypassIp6",
+			hide: true
+		},
+		{
+			headerName: "DNS Bypass TTL",
+			field: "dnsBypassTtl",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "DNS TTL",
+			field: "ccrDnsTtl",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "DSCP",
+			field: "dscp",
+			hide: false,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "ECS Enabled",
+			field: "ecsEnabled",
+			hide: true
+		},
+		{
+			headerName: "Edge Header Rewrite Rules",
+			field: "edgeHeaderRewrite",
+			hide: true
+		},
+		{
+			headerName: "First Header Rewrite Rules",
+			field: "firstHeaderRewrite",
+			hide: true
+		},
+		{
+			headerName: "FQ Pacing Rate",
+			field: "fqPacingRate",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "Geo Limit",
+			field: "geoLimit",
+			hide: true,
+			valueFormatter: params => deliveryServiceUtils.geoLimits[params.data.geoLimit],
+			tooltipValueGetter: params => deliveryServiceUtils.geoLimits[params.data.geoLimit]
+		},
+		{
+			headerName: "Geo Limit Countries",
+			field: "geoLimitCountries",
+			hide: true
+		},
+		{
+			headerName: "Geo Limit Redirect URL",
+			field: "geoLimitRedirectURL",
+			hide: true
+		},
+		{
+			headerName: "Geolocation Provider",
+			field: "geoProvider",
+			hide: true,
+			valueFormatter: params => deliveryServiceUtils.geoProviders[params.data.geoProvider],
+			tooltipValueGetter: params => deliveryServiceUtils.geoProviders[params.data.geoProvider]
+		},
+		{
+			headerName: "Geo Miss Latitude",
+			field: "missLat",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "Geo Miss Longitude",
+			field: "missLong",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "Global Max Mbps",
+			field: "globalMaxMbps",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "Global Max TPS",
+			field: "globalMaxTps",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "HTTP Bypass FQDN",
+			field: "httpBypassFqdn",
+			hide: true
+		},
+		{
+			headerName: "ID",
+			field: "id",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "Info URL",
+			field: "infoUrl",
+			hide: true
+		},
+		{
+			headerName: "Initial Dispersion",
+			field: "initialDispersion",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "Inner Header Rewrite Rules",
+			field: "innerHeaderRewrite",
+			hide: true
+		},
+		{
+			headerName: "IPv6 Routing",
+			field: "ipv6RoutingEnabled",
+			hide: true
+		},
+		{
+			headerName: "Last Header Rewrite Rules",
+			field: "lastHeaderRewrite",
+			hide: true
+		},
+		{
+			headerName: "Last Updated",
+			field: "lastUpdated",
+			hide: true,
+			filter: "agDateColumnFilter",
+		},
+		{
+			headerName: "Long Desc",
+			field: "longDesc",
+			hide: true
+		},
+		{
+			headerName: "Max DNS Answers",
+			field: "maxDnsAnswers",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "Max Origin Connections",
+			field: "maxOriginConnections",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "Max Request Header Bytes",
+			field: "maxRequestHeaderBytes",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "Mid Header Rewrite Rules",
+			field: "midHeaderRewrite",
+			hide: true
+		},
+		{
+			headerName: "Multi-Site Origin",
+			field: "multiSiteOrigin",
+			hide: true
+		},
+		{
+			headerName: "Origin Shield",
+			field: "originShield",
+			hide: true
+		},
+		{
+			headerName: "Origin FQDN",
+			field: "orgServerFqdn",
+			hide: false
+		},
+		{
+			headerName: "Profile",
+			field: "profileName",
+			hide: true
+		},
+		{
+			headerName: "Protocol",
+			field: "protocol",
+			hide: false,
+			valueFormatter: params => deliveryServiceUtils.protocols[params.data.protocol],
+			tooltipValueGetter: params => deliveryServiceUtils.protocols[params.data.protocol]
+		},
+		{
+			headerName: "Qstring Handling",
+			field: "qstringIgnore",
+			hide: true,
+			valueFormatter: params => deliveryServiceUtils.qstrings[params.data.qstringIgnore],
+			tooltipValueGetter: params => deliveryServiceUtils.qstrings[params.data.qstringIgnore]
+		},
+		{
+			headerName: "Range Request Handling",
+			field: "rangeRequestHandling",
+			hide: true,
+			valueFormatter: params => deliveryServiceUtils.rrhs[params.data.rangeRequestHandling],
+			tooltipValueGetter: params => deliveryServiceUtils.rrhs[params.data.rangeRequestHandling]
+		},
+		{
+			headerName: "Regex Remap Expression",
+			field: "regexRemap",
+			hide: true
+		},
+		{
+			headerName: "Regional Geoblocking",
+			field: "regionalGeoBlocking",
+			hide: true
+		},
+		{
+			headerName: "Raw Remap Text",
+			field: "remapText",
+			hide: true
+		},
+		{
+			headerName: "Routing Name",
+			field: "routingName",
+			hide: true
+		},
+		{
+			headerName: "Service Category",
+			field: "serviceCategory",
+			hide: true
+		},
+		{
+			headerName: "Signed",
+			field: "signed",
+			hide: true
+		},
+		{
+			headerName: "Signing Algorithm",
+			field: "signingAlgorithm",
+			hide: true
+		},
+		{
+			headerName: "Range Slice Block Size",
+			field: "rangeSliceBlockSize",
+			hide: true,
+			filter: "agNumberColumnFilter"
+		},
+		{
+			headerName: "Tenant",
+			field: "tenant",
+			hide: false
+		},
+		{
+			headerName: "Topology",
+			field: "topology",
+			hide: false
+		},
+		{
+			headerName: "TR Request Headers",
+			field: "trRequestHeaders",
+			hide: true
+		},
+		{
+			headerName: "TR Response Headers",
+			field: "trResponseHeaders",
+			hide: true
+		},
+		{
+			headerName: "Type",
+			field: "type",
+			hide: false
+		},
+		{
+			headerName: "XML ID (Key)",
+			field: "xmlId",
+			hide: false
+		}
+	];
+
+	let dsRequestsEnabled = propertiesModel.properties.dsRequests.enabled;
+
+	let showCustomCharts = propertiesModel.properties.deliveryServices.charts.customLink.show;
+
+	/**
+	 * @param {string} typeName
+	 */
+	function createDeliveryService(typeName) {
+		locationUtils.navigateToPath(`/delivery-services/new?dsType=${typeName}`);
+	};
+
+	/**
+	 * Opens a dialog that the user uses to clone the given Delivery Service.
+	 *
+	 * @param {{readonly id: number; readonly xmlId: string;}} ds
+	 */
+	async function clone(ds) {
+		const params = {
+			title: `Clone Delivery Service: ${ds.xmlId}`,
+			message: "Please select a content routing category for the clone"
+		};
+
+		const modalInstance = $uibModal.open({
+			templateUrl: "common/modules/dialog/select/dialog.select.tpl.html",
+			controller: "DialogSelectController",
+			size: "md",
+			resolve: {
+				params,
+				// the following represent the 4 categories of delivery services
+				// the ids are arbitrary but the dialog.select dropdown needs them
+				collection: () => [
+					{ id: 1, name: "ANY_MAP" },
+					{ id: 2, name: "DNS" },
+					{ id: 3, name: "HTTP" },
+					{ id: 4, name: "STEERING" }
+				]
+			}
+		});
+
+		const {name} = await modalInstance.result;
+		locationUtils.navigateToPath(`/delivery-services/${ds.id}/clone?dsType=${name}`);
+	};
+
+	/**
+	 * Opens a dialog asking the user for confirmation before deleting the given
+	 * Delivery Service.
+	 *
+	 * @param {{readonly xmlId: string;}} ds
+	 */
+	async function confirmDelete(ds) {
+		const params = {
+			title: `Delete Delivery Service: ${ds.xmlId}`,
+			key: ds.xmlId
+		};
+
+		const modalInstance = $uibModal.open({
+			templateUrl: "common/modules/dialog/delete/dialog.delete.tpl.html",
+			controller: "DialogDeleteController",
+			size: "md",
+			resolve: { params }
+		});
+		try {
+			await modalInstance.result;
+			if (dsRequestsEnabled) {
+				return createDeliveryServiceDeleteRequest(ds);
+			}
+			try {
+				await deliveryServiceService.deleteDeliveryService(ds);
+				messageModel.setMessages([ { level: "success", text: `Delivery service [ ${ds.xmlId} ] deleted` } ], false);
+				$scope.refresh();
+			} catch (fault) {
+				$anchorScroll(); // scrolls window to top
+				messageModel.setMessages(fault.data.alerts, false);
+			}
+		} catch {}
+	};
+
+	/**
+	 * Creates a new DSR to delete the given Delivery Service.
+	 *
+	 * @param {{readonly xmlId: string}} ds
+	 */
+	async function createDeliveryServiceDeleteRequest(ds) {
+		const params = {
+			title: "Delivery Service Delete Request",
+			message: "All delivery service deletions must be reviewed."
+		};
+		const modalInstance = $uibModal.open({
+			templateUrl: "common/modules/dialog/deliveryServiceRequest/dialog.deliveryServiceRequest.tpl.html",
+			controller: "DialogDeliveryServiceRequestController",
+			size: "md",
+			resolve: {
+				params,
+				statuses: () => {
+					const statuses = [
+						{ id: $scope.DRAFT, name: "Save Request as Draft" },
+						{ id: $scope.SUBMITTED, name: "Submit Request for Review and Deployment" }
+					];
+					if (userModel.user.role == propertiesModel.properties.dsRequests.overrideRole) {
+						statuses.push({ id: $scope.COMPLETE, name: "Fulfill Request Immediately" });
+					}
+					return statuses;
+				}
+			}
+		});
+		const options = await modalInstance.result;
+		let status = 'draft';
+		if (options.status.id == $scope.SUBMITTED || options.status.id == $scope.COMPLETE) {
+			status = 'submitted';
+		};
+
+		const dsRequest = {
+			changeType: 'delete',
+			status: status,
+			original: ds
+		};
+
+		// if the user chooses to complete/fulfill the delete request immediately, the ds will be deleted and behind the
+		// scenes a delivery service request will be created and marked as complete
+		if (options.status.id == $scope.COMPLETE) {
+			try {
+				// first delete the ds
+				await deliveryServiceService.deleteDeliveryService(ds);
+				const response = await deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest);
+				const comment = {
+					deliveryServiceRequestId: response.id,
+					value: options.comment
+				};
+				// then create the ds request comment
+				await deliveryServiceRequestService.createDeliveryServiceRequestComment(comment);
+				const promises = [];
+				// assign the ds request
+				promises.push(deliveryServiceRequestService.assignDeliveryServiceRequest(response.id, userModel.user.username));
+				// set the status to 'complete'
+				promises.push(deliveryServiceRequestService.updateDeliveryServiceRequestStatus(response.id, "complete"));
+				// and finally refresh the delivery services table
+				messageModel.setMessages([ { level: "success", text: `Delivery service [ ${ds.xmlId} ] deleted` } ], false);
+				$scope.refresh();
+			} catch (fault) {
+				$anchorScroll(); // scrolls window to top
+				messageModel.setMessages(fault.data.alerts, false);
+			}
+		} else {
+			const response = await deliveryServiceRequestService.createDeliveryServiceRequest(dsRequest);
+			const comment = {
+				deliveryServiceRequestId: response.id,
+				value: options.comment
+			};
+			await deliveryServiceRequestService.createDeliveryServiceRequestComment(comment);
+			const xmlId = (dsRequest.requested) ? dsRequest.requested.xmlId : dsRequest.original.xmlId;
+			messageModel.setMessages([ { level: "success", text: `Created request to ${dsRequest.changeType} the ${xmlId} delivery service` } ], true);
+			locationUtils.navigateToPath("/delivery-service-requests");
+		}
+	}
+
+	/** All of the delivery services - lastUpdated fields converted to actual Dates */
+	$scope.deliveryServices = deliveryServices.map(
+		x => ({...x, lastUpdated: x.lastUpdated ? new Date(x.lastUpdated.replace("+00", "Z").replace(" ", "T")) : x.lastUpdated})
+	);
+
+	$scope.DRAFT = 0;
+	$scope.SUBMITTED = 1;
+	$scope.REJECTED = 2;
+	$scope.PENDING = 3;
+	$scope.COMPLETE = 4;
+
+	/**
+	 * @param {{readonly id: number; type: string}} ds
+	 */
+	function viewCharts(ds) {
+		if (showCustomCharts) {
+			deliveryServiceUtils.openCharts(ds);
+		} else {
+			locationUtils.navigateToPath(`/delivery-services/${ds.id}/charts?dsType=${ds.type}`);
+		}
+	};
+
+	$scope.refresh = function() {
+		$state.reload(); // reloads all the resolves for the view
+	};
+
+	async function selectDSType() {
+		const params = {
+			title: "Create Delivery Service",
+			message: "Please select a content routing category"
+		};
+		const modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/select/dialog.select.tpl.html',
+			controller: 'DialogSelectController',
+			size: 'md',
+			resolve: {
+				params,
+				// the following represent the 4 categories of delivery services
+				// the ids are arbitrary but the dialog.select dropdown needs them
+				collection: () => [
+					{ id: 1, name: 'ANY_MAP' },
+					{ id: 2, name: 'DNS' },
+					{ id: 3, name: 'HTTP' },
+					{ id: 4, name: 'STEERING' }
+				]
+			}
+		});
+		try {
+			const type = await modalInstance.result;
+			createDeliveryService(type.name);
+		} catch {
+			// do nothing
+		}
+	};
+
+	async function compareDSs() {
+		const params = {
+			title: "Compare Delivery Services",
+			message: "Please select 2 delivery services to compare",
+			label: "xmlId"
+		};
+		const modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/compare/dialog.compare.tpl.html',
+			controller: 'DialogCompareController',
+			size: 'md',
+			resolve: {
+				params,
+				collection: deliveryServiceService => deliveryServiceService.getDeliveryServices()
+			}
+		});
+		try {
+			const dss = await modalInstance.result;
+			$location.path(`${$location.path()}/compare/${dss[0].id}/${dss[1].id}`);
+		} catch {
+			// do nothing
+		}
+	};
+
+	$scope.options = {
+		onRowClick: params => {
+			const selection = window.getSelection()?.toString();
+			if(!selection) {
+				locationUtils.navigateToPath(`/delivery-services/${params.data.id}?dsType=${params.data.type}`);
+				// Event is outside the digest cycle, so we need to trigger one.
+				$scope.$apply();
+			}
+		}
+	};
+
+	$scope.dropDownOptions = [
+		{
+			onClick: selectDSType,
+			text: "Create Delivery Service",
+			type: 1
+		},
+		{
+			onClick: compareDSs,
+			text: "Compare Delivery Services",
+			type: 1
+		}
+	];
+
+	$scope.contextMenuOptions = [
+		{
+			getHref: ds => `/delivery-services/${ds.id}?dsType=${ds.type}`,
+			getText: ds => `Open ${ds.xmlId} in a new tab`,
+			newTab: true,
+			type: 2
+		},
+		{type: 0},
+		{
+			getHref: ds => `/delivery-services/${ds.id}?dsType=${ds.type}`,
+			text: "Edit",
+			type: 2
+		},
+		{
+			onClick: clone,
+			text: "Clone",
+			type: 1
+		},
+		{
+			onClick: confirmDelete,
+			text: "Delete",
+			type: 1
+		},
+		{type: 0},
+		{
+			onClick: viewCharts,
+			text: "View Charts",
+			type: 1
+		},
+		{type: 0},
+		{
+			getHref: ds => `#!/delivery-services/${ds.id}/ssl-keys?dsType=${ds.type}`,
+			text: "Manage SSL Keys",
+			type: 2
+		},
+		{
+			getHref: ds => `#!/delivery-services/${ds.id}/url-sig-keys?dsType=${ds.type}`,
+			text: "Manage URL Sig Keys",
+			type: 2
+		},
+		{
+			getHref: ds => `#!/delivery-services/${ds.id}/uri-signing-keys?dsType=${ds.type}`,
+			text: "Manage URI Signing Keys",
+			type: 2
+		},
+		{ type: 0 },
+		{
+			getHref: ds => `#!/delivery-services/${ds.id}/jobs?dsType=${ds.type}`,
+			text: "Manage Invalidation Requests",
+			type: 2
+		},
+		{
+			getHref: ds => `#!/delivery-services/${ds.id}/origins?dsType=${ds.type}`,
+			text: "Manage Origins",
+			type: 2
+		},
+		{
+			getHref: ds => `#!/delivery-services/${ds.id}/regexes?dsType=${ds.type}`,
+			text: "Manage Regexes",
+			type: 2
+		},
+		{
+			getHref: ds => `#!/delivery-services/${ds.id}/required-server-capabilities?dsType=${ds.type}`,
+			text: "Manage Required Server Capabilities",
+			type: 2
+		},
+		{
+			getHref: ds => `#!/delivery-services/${ds.id}/servers?dsType=${ds.type}`,
+			text: "Manage Servers",
+			type: 2
+		},
+		{
+			getHref: ds => `#!/delivery-services/${ds.id}/targets?dsType=${ds.type}`,
+			text: "Manage Targets",
+			type: 2
+		},
+		{
+			getHref: ds => `#!/delivery-services/${ds.id}/static-dns-entries?dsType=${ds.type}`,
+			text: "Manage Static DNS Entries",
+			type: 2
+		},
+
+	];
 };
 
-TableDeliveryServicesController.$inject = ['tableName', 'deliveryServices', 'filter', '$anchorScroll', '$document', '$scope', '$state', '$location', '$uibModal', 'deliveryServiceService', 'deliveryServiceRequestService', 'dateUtils', 'deliveryServiceUtils', 'locationUtils', 'messageModel', 'propertiesModel', 'userModel'];
+TableDeliveryServicesController.$inject = ["tableName", "deliveryServices", "$anchorScroll", "$scope", "$state", "$location", "$uibModal", "deliveryServiceService", "deliveryServiceRequestService", "deliveryServiceUtils", "locationUtils", "messageModel", "propertiesModel", "userModel"];
 module.exports = TableDeliveryServicesController;
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 8c5f25a087..b532ce2fbb 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
@@ -18,104 +18,13 @@ under the License.
 -->
 
 <div class="x_panel">
-    <div class="x_title">
-        <ol class="breadcrumb pull-left">
-            <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 name="createDSMenuItem" role="menuitem"><a ng-click="selectDSType()">Create Delivery Service</a></li>
-                        <li role="menuitem"><a ng-click="compareDSs()">Compare Delivery Services</a></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>
-</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>
+	<common-grid-controller
+		table-title="Delivery Services"
+		table-name="{{tableName}}"
+		options="options"
+		data="deliveryServices"
+		columns="columns"
+		drop-down-options="dropDownOptions"
+		context-menu-options="contextMenuOptions"
+	>
+	</common-grid-controller>
diff --git a/traffic_portal/app/src/common/modules/table/profiles/table.profiles.tpl.html b/traffic_portal/app/src/common/modules/table/profiles/table.profiles.tpl.html
index cf83b4dfd4..7fffcc2c1a 100644
--- a/traffic_portal/app/src/common/modules/table/profiles/table.profiles.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/profiles/table.profiles.tpl.html
@@ -19,7 +19,7 @@ under the License.
 
 <div class="x_panel">
 	<common-grid-controller
-		title="Profiles"
+		table-title="Profiles"
 		table-name="profiles"
 		options="gridOptions"
 		data="profiles"
diff --git a/traffic_portal/app/src/modules/private/cacheStats/cacheStats.tpl.html b/traffic_portal/app/src/modules/private/cacheStats/cacheStats.tpl.html
index b13d971310..1dc109b7c4 100644
--- a/traffic_portal/app/src/modules/private/cacheStats/cacheStats.tpl.html
+++ b/traffic_portal/app/src/modules/private/cacheStats/cacheStats.tpl.html
@@ -18,7 +18,7 @@ under the License.
 -->
 
 <div class="x_panel">
-	<common-grid-controller title="Cache Stats" table-name="cache-stats" options="gridOptions" data="data" columns="columns"
+	<common-grid-controller table-title="Cache Stats" table-name="cache-stats" options="gridOptions" data="data" columns="columns"
 	default-data="defaultData">
 </common-grid-controller>
 </div>
diff --git a/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts b/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts
index 8176ad8f7f..2fedf54cac 100644
--- a/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts
+++ b/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts
@@ -49,7 +49,7 @@ interface AssignRC {
   validationMessage: string;
 }
 export class DeliveryServicePage extends BasePage {
-  private btnCreateNewDeliveryServices = element(by.linkText("Create Delivery Service"));
+  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"))
diff --git a/traffic_portal/test/integration/PageObjects/DeliveryServiceRequestPage.po.ts b/traffic_portal/test/integration/PageObjects/DeliveryServiceRequestPage.po.ts
index 78979385b3..fa848c1e5a 100644
--- a/traffic_portal/test/integration/PageObjects/DeliveryServiceRequestPage.po.ts
+++ b/traffic_portal/test/integration/PageObjects/DeliveryServiceRequestPage.po.ts
@@ -42,7 +42,7 @@ export class DeliveryServicesRequestPage extends BasePage {
   private btnUpdateRequest = element(by.buttonText("Update Request"))
   private btnYes = element(by.buttonText("Yes"))
   private btnMore = element(by.name("moreBtn"))
-  private btnCreateDS = element(by.linkText("Create Delivery Service"));
+  private btnCreateDS = element(by.buttonText("Create Delivery Service"));
   private formDropDown = element(by.name("selectFormDropdown"))
   private txtXmlId = element(by.id("xmlId"));
   private txtDisplayName = element(by.id("displayName"));
@@ -130,4 +130,3 @@ export class DeliveryServicesRequestPage extends BasePage {
     return basePage.GetOutputMessage().then(value => outPutMessage === value);
   }
 }
-