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

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


The following commit(s) were added to refs/heads/master by this push:
     new dcd94e4  TP: adds the ability to delete a job (aka invalidation 
request) (#5086)
dcd94e4 is described below

commit dcd94e4f4c5ad665fcbe1c3846e550138a31870c
Author: Jeremy Mitchell <[email protected]>
AuthorDate: Fri Oct 9 12:10:05 2020 -0600

    TP: adds the ability to delete a job (aka invalidation request) (#5086)
    
    * adds the ability to delete a job (aka invalidation request) in TP
    
    * adds changelog.md entry
    
    * adds the ability to delete invalidation jobs in the view where they are 
filtered by ds
    
    * adds expires column
    
    * allows cell selection and requires doubleclick on servers tables
    
    * replace jquery datatable for jobs with ag-grid
    
    * style more specific to jobs table
    
    * adds a comment
    
    * adds parseInt
    
    * updates CHANGELOG.md
    
    * adds UI tests for jobs
    
    * shows column menu rather than on hover
    
    * adds the ability to multi-sort and always show vertical scrollbar
    
    * adds a default sort to TP for jobs and enables orderby=startTime in the 
api
    
    * removes uneccessary aria-label
---
 CHANGELOG.md                                       |   3 +-
 .../invalidationjobs/invalidationjobs.go           |   1 +
 traffic_portal/app/src/common/api/JobService.js    |  13 ++
 .../app/src/common/modules/table/_table.scss       |  12 +
 .../TableDeliveryServiceJobsController.js          |  23 +-
 .../table.deliveryServiceJobs.tpl.html             |  67 ++++--
 .../modules/table/jobs/TableJobsController.js      | 251 ++++++++++++++++++++-
 .../common/modules/table/jobs/table.jobs.tpl.html  |  67 ++++--
 .../table/servers/TableServersController.js        |  14 +-
 .../modules/private/deliveryServices/jobs/index.js |   2 +-
 .../private/deliveryServices/jobs/list/index.js    |   2 +-
 .../app/src/modules/private/jobs/list/index.js     |   5 +-
 .../deliveryServices/delivery-services-spec.js     |  10 +-
 traffic_portal/test/end_to_end/jobs/jobs-spec.js   |  55 +++++
 .../index.js => test/end_to_end/jobs/pageData.js}  |  28 +--
 15 files changed, 444 insertions(+), 109 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6133df..e25f7e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 - Updated /deliveryservices/{{ID}}/servers to use multiple interfaces in API v3
 - Updated /deliveryservices/{{ID}}/servers/eligible to use multiple interfaces 
in API v3
 - Added the ability to view Hash ID field (aka xmppID) on Traffic Portals' 
server summary page
+- Added the ability to delete invalidation requests in Traffic Portal
 - Added the ability to set TLS config provided here: 
https://golang.org/pkg/crypto/tls/#Config in Traffic Ops
 - Added an indiciator to the Traffic Monitor UI when using a disk backup of 
Traffic ops.
 - Added debugging functionality to CDN-in-a-Box for Traffic Stats.
@@ -95,7 +96,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 - When creating invalidation jobs through TO/TP, if an identical regex is 
detected that overlaps its time, then warnings
 will be returned indicating that overlap exists.
 - Changed Traffic Portal to use Traffic Ops API v3
-- Changed Traffic Portal to use the more performant and powerful ag-grid for 
all server tables.
+- Changed Traffic Portal to use the more performant and powerful ag-grid for 
all server and invalidation request tables.
 - Changed ORT Config Generation to be deterministic, which will prevent 
spurious diffs when nothing actually changed.
 - Changed ORT to find the local ATS config directory and use it when location 
Parameters don't exist for many required configs, including all Delivery 
Service files (Header Rewrites, Regex Remap, URL Sig, URI Signing).
 - Changed ORT to not update ip_allow.config but log an error if it needs 
updating in syncds mode, and only actually update in badass mode.
diff --git 
a/traffic_ops/traffic_ops_golang/invalidationjobs/invalidationjobs.go 
b/traffic_ops/traffic_ops_golang/invalidationjobs/invalidationjobs.go
index 89d6620..c90d1b0 100644
--- a/traffic_ops/traffic_ops_golang/invalidationjobs/invalidationjobs.go
+++ b/traffic_ops/traffic_ops_golang/invalidationjobs/invalidationjobs.go
@@ -212,6 +212,7 @@ func (job *InvalidationJob) Read(h http.Header, useIMS 
bool) ([]interface{}, err
                "id":              dbhelpers.WhereColumnInfo{"job.id", 
api.IsInt},
                "keyword":         dbhelpers.WhereColumnInfo{"job.keyword", 
nil},
                "assetUrl":        dbhelpers.WhereColumnInfo{"job.asset_url", 
nil},
+               "startTime":       dbhelpers.WhereColumnInfo{"job.start_time", 
nil},
                "userId":          dbhelpers.WhereColumnInfo{"job.job_user", 
api.IsInt},
                "createdBy":       dbhelpers.WhereColumnInfo{`(SELECT 
tm_user.username FROM tm_user WHERE tm_user.id=job.job_user)`, nil},
                "deliveryService": dbhelpers.WhereColumnInfo{`(SELECT 
deliveryservice.xml_id FROM deliveryservice WHERE 
deliveryservice.id=job.job_deliveryservice)`, nil},
diff --git a/traffic_portal/app/src/common/api/JobService.js 
b/traffic_portal/app/src/common/api/JobService.js
index 95070d4..42d9ece 100644
--- a/traffic_portal/app/src/common/api/JobService.js
+++ b/traffic_portal/app/src/common/api/JobService.js
@@ -41,6 +41,19 @@ var JobService = function($http, ENV) {
                );
        };
 
+       this.deleteJob = function(id) {
+               return $http.delete(ENV.api['root'] + 'jobs', {params: {id: 
id}}).then(
+                       function(result) {
+                               return result;
+                       },
+                       function(err) {
+                               messageModel.setMessages(err.data.alerts, true);
+                               throw err;
+                       }
+               );
+       };
+
+
 };
 
 JobService.$inject = ['$http', 'ENV'];
diff --git a/traffic_portal/app/src/common/modules/table/_table.scss 
b/traffic_portal/app/src/common/modules/table/_table.scss
index d9f4980..5a0b225 100644
--- a/traffic_portal/app/src/common/modules/table/_table.scss
+++ b/traffic_portal/app/src/common/modules/table/_table.scss
@@ -208,3 +208,15 @@ div.dropdown button.menu-item-button {
         background-color: #f5f5f5;
     }
 }
+
+.jobs-table {
+  .ag-row.active-job {
+    background-color: #dff0d8;
+  }
+  .ag-row.expired-job {
+    background-color: #fcfcfc;
+    .ag-cell {
+      text-decoration: line-through;
+    }
+  }
+}
diff --git 
a/traffic_portal/app/src/common/modules/table/deliveryServiceJobs/TableDeliveryServiceJobsController.js
 
b/traffic_portal/app/src/common/modules/table/deliveryServiceJobs/TableDeliveryServiceJobsController.js
index 58f2deb..a32e0f7 100644
--- 
a/traffic_portal/app/src/common/modules/table/deliveryServiceJobs/TableDeliveryServiceJobsController.js
+++ 
b/traffic_portal/app/src/common/modules/table/deliveryServiceJobs/TableDeliveryServiceJobsController.js
@@ -17,31 +17,18 @@
  * under the License.
  */
 
-var TableDeliveryServiceJobsController = function(deliveryService, jobs, 
$scope, $state, $location, locationUtils) {
+var TableDeliveryServiceJobsController = function(deliveryService, jobs, 
$controller, $scope, $location) {
 
-       $scope.deliveryService = deliveryService;
+       // extends the TableJobsController to inherit common methods
+       angular.extend(this, $controller('TableJobsController', { tableName: 
'dsJobs', jobs: jobs, $scope: $scope }));
 
-       $scope.dsJobs = jobs;
+       $scope.deliveryService = deliveryService;
 
        $scope.createJob = function() {
                $location.path($location.path() + '/new');
        };
 
-       $scope.refresh = function() {
-               $state.reload(); // reloads all the resolves for the view
-       };
-
-       $scope.navigateToPath = locationUtils.navigateToPath;
-
-       angular.element(document).ready(function () {
-               $('#dsJobsTable').dataTable({
-                       "aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, 
"All"]],
-                       "iDisplayLength": 25,
-                       "aaSorting": []
-               });
-       });
-
 };
 
-TableDeliveryServiceJobsController.$inject = ['deliveryService', 'jobs', 
'$scope', '$state', '$location', 'locationUtils'];
+TableDeliveryServiceJobsController.$inject = ['deliveryService', 'jobs', 
'$controller', '$scope', '$location'];
 module.exports = TableDeliveryServiceJobsController;
diff --git 
a/traffic_portal/app/src/common/modules/table/deliveryServiceJobs/table.deliveryServiceJobs.tpl.html
 
b/traffic_portal/app/src/common/modules/table/deliveryServiceJobs/table.deliveryServiceJobs.tpl.html
index 4fb631f..39e6afd 100644
--- 
a/traffic_portal/app/src/common/modules/table/deliveryServiceJobs/table.deliveryServiceJobs.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/table/deliveryServiceJobs/table.deliveryServiceJobs.tpl.html
@@ -20,36 +20,57 @@ under the License.
 <div class="x_panel">
     <div class="x_title">
         <ol class="breadcrumb pull-left">
-            <li><a ng-click="navigateToPath('/delivery-services')">Delivery 
Services</a></li>
-            <li><a ng-click="navigateToPath('/delivery-services/' + 
deliveryService.id + '?type=' + 
deliveryService.type)">{{::deliveryService.xmlId}}</a></li>
+            <li><a href="#!/delivery-services">Delivery Services</a></li>
+            <li><a name="dsLink" ng-href="{{'#!/delivery-services/' + 
deliveryService.id + '?type=' + 
deliveryService.type}}">{{::deliveryService.xmlId}}</a></li>
             <li class="active">Invalidation Requests</li>
         </ol>
         <div class="pull-right">
-            <button class="btn btn-primary" title="Create Invalidation 
Request" ng-click="createJob(deliveryService.id)"><i class="fa 
fa-plus"></i></button>
-            <button class="btn btn-default" title="Refresh" 
ng-click="refresh()"><i class="fa fa-refresh"></i></button>
+            <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" 
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="createJob()">Create Invalidation Request</button></li>
+                        <li class="divider"></li>
+                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="clearColFilters()">Clear Column 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">
-        <br>
-        <table id="dsJobsTable" class="table responsive-utilities jambo_table">
-            <thead>
-            <tr class="headings">
-                <th>Asset URL</th>
-                <th>Parameters</th>
-                <th>Start Time</th>
-                <th>Created By</th>
-            </tr>
-            </thead>
-            <tbody>
-            <tr ng-repeat="j in ::dsJobs">
-                <td data-search="^{{::j.assetUrl}}$">{{::j.assetUrl}}</td>
-                <td data-search="^{{::j.parameters}}$">{{::j.parameters}}</td>
-                <td data-search="^{{::j.startTime}}$">{{::j.startTime}}</td>
-                <td data-search="^{{::j.createdBy}}$">{{::j.createdBy}}</td>
-            </tr>
-            </tbody>
-        </table>
+        <div style="height: 740px;" ag-grid="gridOptions" class="jobs-table 
ag-theme-alpine"></div>
     </div>
 </div>
 
+<menu class="dropdown-menu" ng-style="menuStyle" type="contextmenu" 
ng-show="showMenu">
+    <ul>
+        <li role="menuitem">
+            <button type="button" ng-click="confirmRemoveJob(job, 
$event)">Delete Invalidation Request</button>
+        </li>
+    </ul>
+</menu>
diff --git 
a/traffic_portal/app/src/common/modules/table/jobs/TableJobsController.js 
b/traffic_portal/app/src/common/modules/table/jobs/TableJobsController.js
index 8d26ee9..ede428c 100644
--- a/traffic_portal/app/src/common/modules/table/jobs/TableJobsController.js
+++ b/traffic_portal/app/src/common/modules/table/jobs/TableJobsController.js
@@ -17,10 +17,137 @@
  * under the License.
  */
 
-var TableJobsController = function(jobs, $scope, $state, locationUtils) {
+var TableJobsController = function(tableName, jobs, $document, $scope, $state, 
$uibModal, locationUtils, jobService, messageModel, dateUtils) {
+
+       /**
+        * Gets value to display a default tooltip.
+        */
+       function defaultTooltip(params) {
+               return params.value;
+       }
+
+       function dateCellFormatter(params) {
+               return params.value.toUTCString();
+       }
+
+       columns = [
+               {
+                       headerName: "Delivery Service",
+                       field: "deliveryService",
+                       hide: false
+               },
+               {
+                       headerName: "Asset URL",
+                       field: "assetUrl",
+                       hide: false
+               },
+               {
+                       headerName: "Parameters",
+                       field: "parameters",
+                       hide: false
+               },
+               {
+                       headerName: "Start (UTC)",
+                       field: "startTime",
+                       hide: false,
+                       filter: "agDateColumnFilter",
+                       tooltip: dateCellFormatter,
+                       valueFormatter: dateCellFormatter
+               },
+               {
+                       headerName: "Expires (UTC)",
+                       field: "expires",
+                       hide: false,
+                       filter: "agDateColumnFilter",
+                       tooltip: dateCellFormatter,
+                       valueFormatter: dateCellFormatter
+               },
+               {
+                       headerName: "Created By",
+                       field: "createdBy",
+                       hide: false
+               }
+       ];
+
+       /** All of the jobs - startTime fields converted to actual Dates and 
derived expires field from TTL */
+       $scope.jobs = jobs.map(
+               function(x) {
+                       // need to convert this to a date object for ag-grid 
filter to work properly
+                       x.startTime = new Date(x.startTime.replace("+00", "Z"));
+
+                       // going to derive the expires date from start + TTL 
(hours). Format: TTL:24h
+                       let ttl = parseInt(x.parameters.slice('TTL:'.length, 
x.parameters.length-1), 10);
+                       x.expires = new Date(x.startTime.getTime() + 
ttl*3600*1000);
+               });
 
        $scope.jobs = jobs;
 
+       $scope.quickSearch = '';
+
+       $scope.pageSize = 100;
+
+       /** 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,
+                       tooltip: defaultTooltip
+               },
+               rowClassRules: {
+                       'active-job': function(params) {
+                               return params.data.expires > new Date();
+                       },
+                       'expired-job': function(params) {
+                               return params.data.expires <= new Date();
+                       }
+               },
+               rowData: jobs,
+               pagination: true,
+               paginationPageSize: $scope.pageSize,
+               rowBuffer: 0,
+               onColumnResized: function(params) {
+                       localStorage.setItem(tableName + "_table_columns", 
JSON.stringify($scope.gridOptions.columnApi.getColumnState()));
+               },
+               tooltipShowDelay: 500,
+               allowContextMenuWithControlKey: true,
+               preventDefaultOnContextMenu: true,
+               onCellContextMenu: function(params) {
+                       $scope.showMenu = true;
+                       $scope.menuStyle.left = String(params.event.pageX) + 
"px";
+                       $scope.menuStyle.top = String(params.event.pageY) + 
"px";
+                       $scope.job = params.data;
+                       $scope.$apply();
+               },
+               colResizeDefault: "shift"
+       };
+
+       /** This is used to position the context menu under the cursor. */
+       $scope.menuStyle = {
+               left: 0,
+               top: 0,
+       };
+
+       /** Toggles the visibility of a column that has the ID provided as 
'col'. */
+       $scope.toggleVisibility = function(col) {
+               const visible = 
$scope.gridOptions.columnApi.getColumn(col).isVisible();
+               $scope.gridOptions.columnApi.setColumnVisible(col, !visible);
+       };
+
+       /** Downloads the table as a CSV */
+       $scope.exportCSV = function() {
+               const params = {
+                       allColumns: true,
+                       fileName: "invalidation_requests.csv",
+               };
+               $scope.gridOptions.api.exportDataAsCsv(params);
+       }
+
        $scope.createJob = function() {
                locationUtils.navigateToPath('/jobs/new');
        };
@@ -29,15 +156,127 @@ var TableJobsController = function(jobs, $scope, $state, 
locationUtils) {
                $state.reload(); // reloads all the resolves for the view
        };
 
+       $scope.confirmRemoveJob = function(job, $event) {
+               $event.stopPropagation();
+               const params = {
+                       title: 'Remove Invalidation Request?',
+                       message: 'Are you sure you want to remove the ' + 
job.assetUrl + ' invalidation request?<br><br>' +
+                               'NOTE: The invalidation request may have 
already been applied.'
+               };
+               const modalInstance = $uibModal.open({
+                       templateUrl: 
'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+                       controller: 'DialogConfirmController',
+                       size: 'md',
+                       resolve: {
+                               params: function () {
+                                       return params;
+                               }
+                       }
+               });
+               modalInstance.result.then(function() {
+                       jobService.deleteJob(job.id)
+                               .then(
+                                       function(result) {
+                                               
messageModel.setMessages(result.data.alerts, false);
+                                               $scope.refresh(); // refresh 
the jobs table
+                                       }
+                               );
+               });
+       };
+
+       $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.clearColFilters = function() {
+               $scope.gridOptions.api.setFilterModel(null);
+       };
+
+       /**** Initialization code, including loading user columns from 
localstorage ****/
        angular.element(document).ready(function () {
-               $('#jobsTable').dataTable({
-                       "aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, 
"All"]],
-                       "iDisplayLength": 25,
-                       "aaSorting": []
+               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 filterState = 
JSON.parse(localStorage.getItem(tableName + "_table_filters")) || {};
+                       $scope.gridOptions.api.setFilterModel(filterState);
+               } catch (e) {
+                       console.error("Failure to load stored filter state:", 
e);
+               }
+
+               $scope.gridOptions.api.addEventListener("filterChanged", 
function() {
+                       localStorage.setItem(tableName + "_table_filters", 
JSON.stringify($scope.gridOptions.api.getFilterModel()));
+               });
+
+               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);
+               }
+
+               $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 {
+                               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);
+                       }
+               });
+
+               // clicks outside the context menu will hide it
+               $document.bind("click", function(e) {
+                       $scope.showMenu = false;
+                       e.stopPropagation();
+                       $scope.$apply();
                });
        });
 
 };
 
-TableJobsController.$inject = ['jobs', '$scope', '$state', 'locationUtils'];
+TableJobsController.$inject = ['tableName', 'jobs', '$document', '$scope', 
'$state', '$uibModal', 'locationUtils', 'jobService', 'messageModel', 
'dateUtils'];
 module.exports = TableJobsController;
diff --git 
a/traffic_portal/app/src/common/modules/table/jobs/table.jobs.tpl.html 
b/traffic_portal/app/src/common/modules/table/jobs/table.jobs.tpl.html
index cf4f97d..24b9375 100644
--- a/traffic_portal/app/src/common/modules/table/jobs/table.jobs.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/jobs/table.jobs.tpl.html
@@ -22,34 +22,53 @@ under the License.
         <ol class="breadcrumb pull-left">
             <li class="active">Invalidation Requests</li>
         </ol>
-        <div class="pull-right" role="group" ng-show="!settings.isNew">
-            <button class="btn btn-primary" title="Create Invalidation 
Request" ng-click="createJob()"><i class="fa fa-plus"></i></button>
-            <button class="btn btn-default" title="Refresh" 
ng-click="refresh()"><i class="fa fa-refresh"></i></button>
+        <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" 
class="form-control" placeholder="100" ng-model="pageSize" 
ng-change="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 
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 name="createJobMenuItem" 
class="menu-item-button" type="button" ng-click="createJob()">Create 
Invalidation Request</button></li>
+                        <li class="divider"></li>
+                        <li role="menuitem"><button class="menu-item-button" 
type="button" ng-click="clearColFilters()">Clear Column 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">
-        <br>
-        <table id="jobsTable" class="table responsive-utilities jambo_table">
-            <thead>
-            <tr class="headings">
-                <th>Delivery Service</th>
-                <th>Asset URL</th>
-                <th>Parameters</th>
-                <th>Start</th>
-                <th>Created By</th>
-            </tr>
-            </thead>
-            <tbody>
-            <tr ng-repeat="j in ::jobs">
-                <td 
data-search="^{{::j.deliveryService}}$">{{::j.deliveryService}}</td>
-                <td data-search="^{{::j.assetUrl}}$">{{::j.assetUrl}}</td>
-                <td data-search="^{{::j.parameters}}$">{{::j.parameters}}</td>
-                <td data-search="^{{::j.startTime}}$">{{::j.startTime}}</td>
-                <td data-search="^{{::j.createdBy}}$">{{::j.createdBy}}</td>
-            </tr>
-            </tbody>
-        </table>
+        <div style="height: 740px;" ag-grid="gridOptions" class="jobs-table 
ag-theme-alpine"></div>
     </div>
 </div>
 
+<menu class="dropdown-menu" ng-style="menuStyle" type="contextmenu" 
ng-show="showMenu">
+    <ul>
+        <li role="menuitem">
+            <button type="button" ng-click="confirmRemoveJob(job, 
$event)">Delete Invalidation Request</button>
+        </li>
+    </ul>
+</menu>
diff --git 
a/traffic_portal/app/src/common/modules/table/servers/TableServersController.js 
b/traffic_portal/app/src/common/modules/table/servers/TableServersController.js
index 5fc48d0..e2e63e8 100644
--- 
a/traffic_portal/app/src/common/modules/table/servers/TableServersController.js
+++ 
b/traffic_portal/app/src/common/modules/table/servers/TableServersController.js
@@ -305,14 +305,18 @@ var TableServersController = function(tableName, servers, 
filter, $scope, $state
                        sshCellRenderer: SSHCellRenderer,
                        updateCellRenderer: UpdateCellRenderer
                },
+               onRowDoubleClicked: function(params) {
+                       locationUtils.navigateToPath('/servers/' + 
params.data.id);
+                       // Event is outside the digest cycle, so we need to 
trigger one.
+                       $scope.$apply();
+               },
                columnDefs: columns,
+               enableCellTextSelection:true,
+               suppressMenuHide: true,
+               multiSortKey: 'ctrl',
+               alwaysShowVerticalScroll: true,
                defaultColDef: {
                        filter: true,
-                       onCellClicked: function(params) {
-                                       
locationUtils.navigateToPath('/servers/' + params.data.id);
-                                       // Event is outside the digest cycle, 
so we need to trigger one.
-                                       $scope.$apply();
-                               },
                        sortable: true,
                        resizable: true,
                        tooltip: defaultTooltip
diff --git 
a/traffic_portal/app/src/modules/private/deliveryServices/jobs/index.js 
b/traffic_portal/app/src/modules/private/deliveryServices/jobs/index.js
index 0af31df..e6c8996 100644
--- a/traffic_portal/app/src/modules/private/deliveryServices/jobs/index.js
+++ b/traffic_portal/app/src/modules/private/deliveryServices/jobs/index.js
@@ -22,7 +22,7 @@ module.exports = 
angular.module('trafficPortal.private.deliveryServices.jobs', [
        .config(function($stateProvider, $urlRouterProvider) {
                $stateProvider
                        .state('trafficPortal.private.deliveryServices.jobs', {
-                               url: '/{deliveryServiceId}/jobs',
+                               url: '/{deliveryServiceId}/jobs?type',
                                abstract: true,
                                views: {
                                        deliveryServicesContent: {
diff --git 
a/traffic_portal/app/src/modules/private/deliveryServices/jobs/list/index.js 
b/traffic_portal/app/src/modules/private/deliveryServices/jobs/list/index.js
index 7e789c2..b576ce9 100644
--- a/traffic_portal/app/src/modules/private/deliveryServices/jobs/list/index.js
+++ b/traffic_portal/app/src/modules/private/deliveryServices/jobs/list/index.js
@@ -31,7 +31,7 @@ module.exports = 
angular.module('trafficPortal.private.deliveryServices.jobs.lis
                                                                return 
deliveryServiceService.getDeliveryService($stateParams.deliveryServiceId);
                                                        },
                                                        jobs: 
function($stateParams, jobService) {
-                                                               return 
jobService.getJobs({ dsId: $stateParams.deliveryServiceId });
+                                                               return 
jobService.getJobs({ dsId: $stateParams.deliveryServiceId, orderby: 
'startTime', sortOrder: 'desc' });
                                                        }
                                                }
                                        }
diff --git a/traffic_portal/app/src/modules/private/jobs/list/index.js 
b/traffic_portal/app/src/modules/private/jobs/list/index.js
index b863aa4..6e77cb8 100644
--- a/traffic_portal/app/src/modules/private/jobs/list/index.js
+++ b/traffic_portal/app/src/modules/private/jobs/list/index.js
@@ -27,8 +27,11 @@ module.exports = 
angular.module('trafficPortal.private.jobs.list', [])
                                                templateUrl: 
'common/modules/table/jobs/table.jobs.tpl.html',
                                                controller: 
'TableJobsController',
                                                resolve: {
+                                                       tableName: function() {
+                                                               return 'jobs';
+                                                       },
                                                        jobs: 
function(jobService) {
-                                                               return 
jobService.getJobs();
+                                                               return 
jobService.getJobs({ orderby: 'startTime', sortOrder: 'desc' });
                                                        }
                                                }
                                        }
diff --git 
a/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js 
b/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js
index 5008ac1..0ff266e 100644
--- a/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js
+++ b/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js
@@ -367,19 +367,11 @@ describe('Traffic Portal Delivery Services Suite', 
function() {
                expect(pageData.selectServersMenuItem.isEnabled()).toBe(false);
        });
 
-       it('should navigate back to the HTTP delivery service and delete it', 
function() {
-               console.log('Deleting ' + mockVals.httpXmlId);
-               pageData.dsLink.click();
-               pageData.deleteButton.click();
-               pageData.confirmWithNameInput.sendKeys(mockVals.httpXmlId);
-               pageData.deletePermanentlyButton.click();
-               
expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/delivery-services");
-       });
-
        // Steering delivery service
 
        it('should click new delivery service and select Steering category from 
the dropdown', function() {
                console.log('Clicked Create New and selecting Steering');
+               browser.setLocation("delivery-services");
                
browser.driver.findElement(by.name('createDeliveryServiceButton')).click();
                expect(pageData.selectFormSubmitButton.isEnabled()).toBe(false);
                
browser.driver.findElement(by.name('selectFormDropdown')).sendKeys('STEERING');
diff --git a/traffic_portal/test/end_to_end/jobs/jobs-spec.js 
b/traffic_portal/test/end_to_end/jobs/jobs-spec.js
new file mode 100644
index 0000000..ca594ab
--- /dev/null
+++ b/traffic_portal/test/end_to_end/jobs/jobs-spec.js
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var pd = require('./pageData.js');
+var cfunc = require('../common/commonFunctions.js');
+
+describe('Traffic Portal Jobs Test Suite', function() {
+       const pageData = new pd();
+       const commonFunctions = new cfunc();
+       const newJob = {
+               regex: '/foo.png',
+               ttl: 24
+       };
+
+       it('should go to the jobs page', function() {
+               console.log("Go to the jobs page");
+               browser.setLocation("jobs");
+               
expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/jobs");
+       });
+
+       it('should open new job form page', function() {
+               console.log("Open new job form page");
+               pageData.moreBtn.click();
+               pageData.createJobMenuItem.click();
+               
expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/jobs/new");
+       });
+
+       it('should build a new job', function () {
+               console.log("Building a new job");
+               expect(pageData.createButton.isEnabled()).toBe(false);
+               commonFunctions.selectDropdownbyNum(pageData.deliveryservice, 
1);
+               pageData.regex.sendKeys(newJob.regex);
+               pageData.ttl.sendKeys(newJob.ttl);
+               expect(pageData.createButton.isEnabled()).toBe(true);
+               pageData.createButton.click();
+               
expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/jobs");
+       });
+
+});
diff --git a/traffic_portal/app/src/modules/private/jobs/list/index.js 
b/traffic_portal/test/end_to_end/jobs/pageData.js
similarity index 60%
copy from traffic_portal/app/src/modules/private/jobs/list/index.js
copy to traffic_portal/test/end_to_end/jobs/pageData.js
index b863aa4..422ee60 100644
--- a/traffic_portal/app/src/modules/private/jobs/list/index.js
+++ b/traffic_portal/test/end_to_end/jobs/pageData.js
@@ -17,23 +17,11 @@
  * under the License.
  */
 
-module.exports = angular.module('trafficPortal.private.jobs.list', [])
-       .config(function($stateProvider, $urlRouterProvider) {
-               $stateProvider
-                       .state('trafficPortal.private.jobs.list', {
-                               url: '',
-                               views: {
-                                       jobsContent: {
-                                               templateUrl: 
'common/modules/table/jobs/table.jobs.tpl.html',
-                                               controller: 
'TableJobsController',
-                                               resolve: {
-                                                       jobs: 
function(jobService) {
-                                                               return 
jobService.getJobs();
-                                                       }
-                                               }
-                                       }
-                               }
-                       })
-               ;
-               $urlRouterProvider.otherwise('/');
-       });
+module.exports = function(){
+       this.moreBtn=element(by.name('moreBtn'));
+       this.createJobMenuItem=element(by.name('createJobMenuItem'));
+       this.regex=element(by.name('regex'));
+       this.ttl=element(by.name('ttl'));
+       this.deliveryservice=element(by.name('deliveryservice'));
+       this.createButton=element(by.buttonText('Create'));
+};

Reply via email to