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();
+ }
}