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 5cff4be  Add the ability to edit and delete existing Jobs to TPv2 
(#6453)
5cff4be is described below

commit 5cff4be9bc3eeba1dde225f61212e6767dda2496
Author: ocket8888 <[email protected]>
AuthorDate: Wed Jan 12 14:10:38 2022 -0700

    Add the ability to edit and delete existing Jobs to TPv2 (#6453)
    
    * Add the ability to edit and delete existing Jobs
    
    * Fix not setting appropriate defaults when editing jobs
    
    * Fix capitalization
---
 .../invalidation-jobs.component.html               |  26 ++++-
 .../invalidation-jobs.component.spec.ts            |  21 +++-
 .../invalidation-jobs.component.ts                 |  81 ++++++++++-----
 .../new-invalidation-job-dialog.component.spec.ts  |  20 +++-
 .../new-invalidation-job-dialog.component.ts       | 109 ++++++++++++++++++---
 .../traffic-portal/src/app/models/invalidation.ts  |   4 +-
 .../src/app/shared/api/InvalidationJobService.ts   |  32 +++++-
 7 files changed, 249 insertions(+), 44 deletions(-)

diff --git 
a/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.html
 
b/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.html
index d1f69c7..e631802 100644
--- 
a/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.html
+++ 
b/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.html
@@ -13,6 +13,30 @@ limitations under the License.
 -->
 <tp-header title="{{deliveryservice ? deliveryservice.displayName : 
'Loading'}} - Content Invalidation Jobs"></tp-header>
 <ul>
-       <li *ngFor="let j of jobs" [hidden]="endDate(j) < 
now"><code>{{j.assetUrl}}</code> (active from <time 
[dateTime]="j.startTime">{{j.startTime | date:'medium'}}</time> to <time 
[dateTime]="endDate(j)">{{endDate(j) | date:'medium'}})</time></li>
+       <li *ngFor="let j of jobs">
+               <code>{{j.assetUrl}}</code> (active from <time 
[dateTime]="j.startTime">{{j.startTime | date:'medium'}}</time> to <time 
[dateTime]="endDate(j)">{{endDate(j) | date:'medium'}})</time>
+               <button
+                       mat-icon-button
+                       type="button"
+                       color="accent"
+                       title="Edit this content invalidation job"
+                       aria-label="Edit this content invalidation job"
+                       [disabled]="j.startTime <= now"
+                       (click)="editJob(j)"
+               >
+                       <fa-icon [icon]="editIcon"></fa-icon>
+               </button>
+               <button
+                       mat-icon-button
+                       type="button"
+                       color="warn"
+                       title="Delete this content invalidation job"
+                       aria-label="Delete this content invalidation job"
+                       [disabled]="isInProgress(j)"
+                       (click)="deleteJob(j.id)"
+               >
+                       <fa-icon [icon]="deleteIcon"></fa-icon>
+               </button>
+       </li>
 </ul>
 <button mat-fab type="button" id="new" title="Create a new content 
invalidation job for this Delivery Service" (click)="newJob()"><fa-icon 
[icon]="addIcon"></fa-icon></button>
diff --git 
a/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.spec.ts
 
b/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.spec.ts
index 2550ca2..c4b1b24 100644
--- 
a/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.spec.ts
+++ 
b/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.spec.ts
@@ -24,7 +24,7 @@ import {DeliveryServiceService, InvalidationJobService, 
UserService} from "src/a
 import { CurrentUserService } from 
"src/app/shared/currentUser/current-user.service";
 import { CustomvalidityDirective } from 
"../../shared/validation/customvalidity.directive";
 import { OpenableDirective } from "../../shared/openable/openable.directive";
-import { DeliveryService, GeoLimit, GeoProvider, InvalidationJob } from 
"../../models";
+import { DeliveryService, GeoLimit, GeoProvider, InvalidationJob, JobType } 
from "../../models";
 import {TpHeaderComponent} from "../../shared/tp-header/tp-header.component";
 import { InvalidationJobsComponent } from "./invalidation-jobs.component";
 
@@ -95,6 +95,25 @@ describe("InvalidationJobsComponent", () => {
                expect(component).toBeTruthy();
        });
 
+       it("determines in-progress state", ()=>{
+               const j = {
+                       assetUrl: "",
+                       createdBy: "",
+                       deliveryService: "",
+                       id: -1,
+                       keyword: JobType.PURGE,
+                       parameters: "TTL:1h",
+                       startTime: new Date(component.now)
+               };
+               j.startTime.setDate(j.startTime.getDate()-1);
+               expect(component.isInProgress(j)).toBeFalse();
+               j.startTime = new Date(component.now);
+               j.startTime.setMinutes(j.startTime.getMinutes()-30);
+               expect(component.isInProgress(j)).toBeTrue();
+               j.startTime = new Date();
+               expect(component.isInProgress(j)).toBeFalse();
+       });
+
        afterAll(() => {
                try{
                        TestBed.resetTestingModule();
diff --git 
a/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.ts
 
b/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.ts
index f05dc5a..9a26732 100644
--- 
a/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.ts
+++ 
b/experimental/traffic-portal/src/app/core/invalidation-jobs/invalidation-jobs.component.ts
@@ -14,7 +14,7 @@
 import { Component, OnInit } from "@angular/core";
 import { MatDialog } from "@angular/material/dialog";
 import { ActivatedRoute } from "@angular/router";
-import { faPlus } from "@fortawesome/free-solid-svg-icons";
+import { faPlus, faTrash, faPencilAlt } from 
"@fortawesome/free-solid-svg-icons";
 
 import { defaultDeliveryService, DeliveryService, InvalidationJob } from 
"../../models";
 import { DeliveryServiceService, InvalidationJobService } from 
"../../shared/api";
@@ -41,14 +41,17 @@ export class InvalidationJobsComponent implements OnInit {
        public now: Date = new Date();
 
        /** The ID of the Delivery Service to which the displayed Jobs belong. 
*/
-       private dsId = -1;
+       private dsID = -1;
 
        /** The icon for the "Create a new Job" FAB. */
        public readonly addIcon = faPlus;
 
-       /**
-        * Constructor.
-        */
+       /** The icon for the Job deletion button. */
+       public readonly deleteIcon = faTrash;
+
+       /** The icon for the Job edit button. */
+       public readonly editIcon = faPencilAlt;
+
        constructor(
                private readonly route: ActivatedRoute,
                private readonly jobAPI: InvalidationJobService,
@@ -70,20 +73,13 @@ export class InvalidationJobsComponent implements OnInit {
                        console.error("Missing route 'id' parameter");
                        return;
                }
-               this.dsId = parseInt(idParam, 10);
-               this.jobAPI.getInvalidationJobs({dsID: this.dsId}).then(
+               this.dsID = parseInt(idParam, 10);
+               this.jobAPI.getInvalidationJobs({dsID: this.dsID}).then(
                        r => {
-                               // The values returned by the API are not 
RFC-compliant at the time of this writing,
-                               // so we need to do some pre-processing on them.
-                               for (const j of r) {
-                                       const tmp = 
Array.from(String(j.startTime).split(" ").join("T"));
-                                       tmp.splice(-3, 3);
-                                       j.startTime = new Date(tmp.join(""));
-                                       this.jobs.push(j);
-                               }
+                               this.jobs = r;
                        }
                );
-               this.dsAPI.getDeliveryServices(this.dsId).then(
+               this.dsAPI.getDeliveryServices(this.dsID).then(
                        r => {
                                this.deliveryservice = r;
                        }
@@ -91,6 +87,26 @@ export class InvalidationJobsComponent implements OnInit {
        }
 
        /**
+        * Gets whether or not a Job is in-progress.
+        *
+        * @param j The Job to check.
+        * @returns Whether or not `j` is currently in-progress.
+        */
+       public isInProgress(j: InvalidationJob): boolean {
+               return j.startTime <= this.now && this.endDate(j) >= this.now;
+       }
+
+       /**
+        * Handles a click on a
+        *
+        * @param j The ID of the Job to delete.
+        */
+       public async deleteJob(j: number): Promise<void> {
+               await this.jobAPI.deleteInvalidationJob(j);
+               this.jobs = await this.jobAPI.getInvalidationJobs();
+       }
+
+       /**
         * Gets the ending date and time for a content invalidation job.
         *
         * @param j The job from which to extract an end date.
@@ -100,7 +116,7 @@ export class InvalidationJobsComponent implements OnInit {
                if (!j.parameters) {
                        throw new Error("cannot get end date for job with no 
parameters");
                }
-               const tmp = j.parameters.split(":");
+               const tmp = j.parameters.replace(/h$/, "").split(":");
                if (tmp.length !== 2) {
                        throw new Error(`Malformed job parameters: 
"${j.parameters}" (id: ${j.id})`);
                }
@@ -121,23 +137,36 @@ export class InvalidationJobsComponent implements OnInit {
         * @param e The DOM event that triggered the creation.
         */
        public newJob(): void {
-               const dialogRef = 
this.dialog.open(NewInvalidationJobDialogComponent, {data: this.dsId});
+               const dialogRef = 
this.dialog.open(NewInvalidationJobDialogComponent, {data: {dsID: this.dsID}});
                dialogRef.afterClosed().subscribe(
                        (created) => {
                                if (created) {
-                                       this.jobAPI.getInvalidationJobs({dsID: 
this.dsId}).then(
+                                       this.jobAPI.getInvalidationJobs({dsID: 
this.dsID}).then(
                                                resp => {
-                                                       this.jobs = new 
Array<InvalidationJob>();
-                                                       for (const j of resp) {
-                                                               const tmp = 
Array.from(String(j.startTime).replace(" ", "T"));
-                                                               tmp.splice(-3, 
3);
-                                                               j.startTime = 
new Date(tmp.join(""));
-                                                               
this.jobs.push(j);
-                                                       }
+                                                       this.jobs = resp;
                                                }
                                        );
                                }
                        }
                );
        }
+
+       /**
+        * Handles a user clicking on a Job's "edit" button by opening the edit
+        * dialog.
+        *
+        * @param job The Job to be edited.
+        */
+       public editJob(job: InvalidationJob): void {
+               const dialogRef = 
this.dialog.open(NewInvalidationJobDialogComponent, {data: {dsID: this.dsID, 
job}});
+               dialogRef.afterClosed().subscribe(
+                       created => {
+                               if (created) {
+                                       this.jobAPI.getInvalidationJobs({dsID: 
this.dsID}).then(
+                                               resp => this.jobs = resp
+                                       );
+                               }
+                       }
+               );
+       }
 }
diff --git 
a/experimental/traffic-portal/src/app/core/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.spec.ts
 
b/experimental/traffic-portal/src/app/core/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.spec.ts
index 9891bea..9975728 100644
--- 
a/experimental/traffic-portal/src/app/core/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.spec.ts
+++ 
b/experimental/traffic-portal/src/app/core/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.spec.ts
@@ -16,7 +16,7 @@ import { ComponentFixture, TestBed } from 
"@angular/core/testing";
 import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from 
"@angular/material/dialog";
 
 import {InvalidationJobService} from "../../../shared/api";
-import { NewInvalidationJobDialogComponent } from 
"./new-invalidation-job-dialog.component";
+import { NewInvalidationJobDialogComponent, sanitizedRegExpString, 
timeStringFromDate } from "./new-invalidation-job-dialog.component";
 
 describe("NewInvalidationJobDialogComponent", () => {
        let component: NewInvalidationJobDialogComponent;
@@ -34,7 +34,7 @@ describe("NewInvalidationJobDialogComponent", () => {
                                {provide: MatDialogRef, useValue: {close: (): 
void => {
                                        console.log("dialog closed");
                                }}},
-                               {provide: MAT_DIALOG_DATA, useValue: -1},
+                               {provide: MAT_DIALOG_DATA, useValue: {dsID: 
-1}},
                                { provide: InvalidationJobService, useValue: 
mockAPIService}
                        ]
                }).compileComponents();
@@ -50,3 +50,19 @@ describe("NewInvalidationJobDialogComponent", () => {
                expect(component).toBeTruthy();
        });
 });
+
+describe("NewInvalidationJobDialogComponent utility functions", () => {
+       it("gets a time string from a Date", ()=>{
+               const d = new Date();
+               d.setHours(0);
+               d.setMinutes(0);
+               expect(timeStringFromDate(d)).toBe("00:00");
+               d.setHours(12);
+               d.setMinutes(34);
+               expect(timeStringFromDate(d)).toBe("12:34");
+       });
+       it("sanitizes regular expressions", ()=>{
+               
expect(sanitizedRegExpString(/\/.+\/my\/path\.jpg/)).toBe("/.+/my/path\\.jpg");
+               expect(sanitizedRegExpString(new 
RegExp("\\/path\\/to\\/content\\/.+\\.m3u8"))).toBe("/path/to/content/.+\\.m3u8");
+       });
+});
diff --git 
a/experimental/traffic-portal/src/app/core/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.ts
 
b/experimental/traffic-portal/src/app/core/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.ts
index 49c2ca9..5a7dd70 100644
--- 
a/experimental/traffic-portal/src/app/core/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.ts
+++ 
b/experimental/traffic-portal/src/app/core/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component.ts
@@ -16,9 +16,52 @@ import { FormControl } from "@angular/forms";
 import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog";
 import { Subject } from "rxjs";
 
+import type { InvalidationJob } from "src/app/models";
 import { InvalidationJobService } from "src/app/shared/api";
 
 /**
+ * Gets the time part of a Date as a string.
+ *
+ * @param d The date to convert.
+ * @returns A string that represents the *local* time of the given date in
+ * `HH:mm` format.
+ */
+export function timeStringFromDate(d: Date): string {
+       const hours = String(d.getHours()).padStart(2, "0");
+       const minutes = String(d.getMinutes()).padStart(2, "0");
+       return `${hours}:${minutes}`;
+}
+
+/** The type of parameters passable to the dialog. */
+interface DialogData {
+       /** The ID of the Delivery Service to which the created/edited Job 
belongs. */
+       dsID: number;
+       /** If passed, the dialog will edit this Job instead of creating a new 
one. */
+       job?: InvalidationJob;
+}
+
+/**
+ * Gets the string representation of a user-entered regular expression (for
+ * Content Invalidation Jobs).
+ *
+ * Users have a tendency to assign undue importance to '/' because of its
+ * ubiquitous use in the `sed` command line utility examples and snippets
+ * online. This will un-escape any '/'s that the user escaped.
+ *
+ * @example
+ * const r = new RegExp("/.+\\/mypath\\/.+\.jpg/");
+ * console.log(sanitizedRegExpString(r));
+ * // Output: ".+/mypath/.+\.jpg"
+ *
+ * @param r A regular expression entered by a user.
+ * @returns The string representation of the regexp, with unnecessary bits
+ * removed.
+ */
+export function sanitizedRegExpString(r: RegExp): string {
+       return r.toString().replace(/^\/|\/$/g, "").replace(/\\\//g, "/");
+}
+
+/**
  * This is the controller for the dialog box that opens when the user creates
  * a new Content Invalidation Job.
  */
@@ -28,8 +71,9 @@ import { InvalidationJobService } from "src/app/shared/api";
        templateUrl: "./new-invalidation-job-dialog.component.html",
 })
 export class NewInvalidationJobDialogComponent {
+
        /** The minimum Start Date that may be selected. */
-       public readonly startMin = new Date();
+       public startMin = new Date();
        /** The minimum Start Time that may be selected. */
        public startMinTime: string;
 
@@ -45,16 +89,30 @@ export class NewInvalidationJobDialogComponent {
        /** A subscribable that tracks whether the new job's regexp is valid. */
        public readonly regexpIsValid = new Subject<string>();
 
+       private readonly job: InvalidationJob | undefined;
+       private readonly dsID: number;
+
        constructor(
                private readonly dialogRef: 
MatDialogRef<NewInvalidationJobDialogComponent>,
                private readonly jobAPI: InvalidationJobService,
-               @Inject(MAT_DIALOG_DATA) private readonly dsID: number
+               @Inject(MAT_DIALOG_DATA) data: DialogData
        ) {
-               this.startDate.setDate(this.startDate.getDate()+1);
-               const hours = String(this.startMin.getHours()).padStart(2, "0");
-               const minutes = String(this.startMin.getMinutes()).padStart(2, 
"0");
-               this.startMinTime = `${hours}:${minutes}`;
-               this.startTime.setValue(this.startMinTime);
+               this.job = data.job;
+               if (this.job) {
+                       this.startDate = this.job.startTime;
+                       const startTime  = 
timeStringFromDate(this.job.startTime);
+                       this.startMinTime = startTime;
+                       this.startTime.setValue(startTime);
+                       
this.ttl.setValue(parseInt(this.job.parameters.split(":")[1], 10));
+                       const regexp = 
this.job.assetUrl.split("/").slice(3).join("/") || "/";
+                       this.regexp.setValue(regexp);
+               } else {
+                       this.startDate.setDate(this.startDate.getDate()+1);
+                       this.startMinTime = timeStringFromDate(this.startMin);
+                       this.startTime.setValue(this.startMinTime);
+               }
+
+               this.dsID = data.dsID;
        }
 
        /**
@@ -67,15 +125,39 @@ export class NewInvalidationJobDialogComponent {
                        this.startDate.getMonth() <= this.startMin.getMonth() &&
                        this.startDate.getDate() <= this.startMin.getDate()
                ) {
-                       const hours = 
String(this.startMin.getHours()).padStart(2, "0");
-                       const minutes = 
String(this.startMin.getMinutes()).padStart(2, "0");
-                       this.startMinTime = `${hours}:${minutes}`;
+                       this.startMinTime = timeStringFromDate(this.startMin);
                } else {
                        this.startMinTime = "00:00";
                }
        }
 
        /**
+        * Updates a content invalidation job based on passed pre-parsed data in
+        * combination with the component's state.
+        *
+        * @param j The Job being edited (in its original form).
+        * @param re The Job's new Regular Expression (pre-parsed from a form
+        * control).
+        * @param startTime The Job's new Start Time (pre-parsed from Form 
Controls).
+        */
+       private editJob(j: InvalidationJob, re: RegExp, startTime: Date): void {
+               const job = {
+                       ...j,
+                       parameters: `TTL:${this.ttl.value as number}`,
+                       startTime
+               };
+               job.assetUrl = `${job.assetUrl.split("/").slice(0, 
3).join("/")}/${sanitizedRegExpString(re)}`;
+
+               this.jobAPI.updateInvalidationJob(job).then(
+                       ()=>this.dialogRef.close(true)
+               ).catch(
+                       e => {
+                               console.error("error:", e);
+                       }
+               );
+       }
+
+       /**
         * Handles submission of the content invalidation job creation form.
         *
         * @param event The form submission event, which must be 
.preventDefault'd.
@@ -93,10 +175,15 @@ export class NewInvalidationJobDialogComponent {
                }
 
                const startTime = new Date(this.startDate);
-               const [hours, minutes] = (this.startTime.value as 
string).split(":").map(x=>Number(x));
+               const [hours, minutes] = (this.startTime.value as 
`${number}:${number}`).split(":").map(x=>Number(x));
                startTime.setHours(hours);
                startTime.setMinutes(minutes);
 
+               if (this.job) {
+                       this.editJob(this.job, re, startTime);
+                       return;
+               }
+
                const job = {
                        deliveryService: this.dsID,
                        regex: re.toString().replace(/^\/|\/$/g, 
"").replace("\\/", "/"),
diff --git a/experimental/traffic-portal/src/app/models/invalidation.ts 
b/experimental/traffic-portal/src/app/models/invalidation.ts
index 1de0cdf..538fc5e 100644
--- a/experimental/traffic-portal/src/app/models/invalidation.ts
+++ b/experimental/traffic-portal/src/app/models/invalidation.ts
@@ -26,7 +26,7 @@ export interface InvalidationJob {
         * A regular expression that matches content to be "invalidated" or
         * "revalidated".
         */
-       assetUrl: RegExp;
+       assetUrl: string;
        /**
         * The name of the user that created the Job.
         */
@@ -34,7 +34,7 @@ export interface InvalidationJob {
        /** The XMLID of the Delivery Service within which the Job will 
operate. */
        deliveryService: string;
        /** An integral, unique identifier for this Job. */
-       id: number;
+       readonly id: number;
        /** The type of Job. */
        keyword: JobType;
 
diff --git 
a/experimental/traffic-portal/src/app/shared/api/InvalidationJobService.ts 
b/experimental/traffic-portal/src/app/shared/api/InvalidationJobService.ts
index 0737303..c9ab603 100644
--- a/experimental/traffic-portal/src/app/shared/api/InvalidationJobService.ts
+++ b/experimental/traffic-portal/src/app/shared/api/InvalidationJobService.ts
@@ -76,7 +76,17 @@ export class InvalidationJobService extends APIService {
                                params.userId = String(opts.user.id);
                        }
                }
-               return this.get<Array<InvalidationJob>>(path, undefined, 
params).toPromise().catch(
+               return this.get<Array<InvalidationJob>>(path, undefined, 
params).toPromise().then(
+                       js => {
+                               const jobs = new Array<InvalidationJob>();
+                               for (const j of js) {
+                                       const tmp = 
String(j.startTime).replace(" ", "T").replace("+00", "Z");
+                                       j.startTime = new Date(tmp);
+                                       jobs.push(j);
+                               }
+                               return jobs;
+                       }
+               ).catch(
                        e => {
                                console.error("Failed to get Invalidation 
Jobs:", e);
                                return [];
@@ -97,4 +107,24 @@ export class InvalidationJobService extends APIService {
                        () => false
                );
        }
+
+       /**
+        * Updates a Job by replacing it with a new definition.
+        *
+        * @param job The new definition of the Job.
+        * @returns The edited Job as returned by the server.
+        */
+       public async updateInvalidationJob(job: InvalidationJob): 
Promise<InvalidationJob> {
+               return this.put<InvalidationJob>("jobs", job, {id: 
String(job.id)}).toPromise();
+       }
+
+       /**
+        * Deletes a Job.
+        *
+        * @param id The ID of the Job to delete.
+        * @returns The deleted Job.
+        */
+       public async deleteInvalidationJob(id: number): 
Promise<InvalidationJob> {
+               return this.delete<InvalidationJob>("jobs", undefined, {id: 
String(id)}).toPromise();
+       }
 }

Reply via email to