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 2023/04/18 16:28:34 UTC
[trafficcontrol] branch master updated: Yuengling profile table parity (#7434)
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 54e509bfe6 Yuengling profile table parity (#7434)
54e509bfe6 is described below
commit 54e509bfe60b073cb488a088c22c2337b35dff9b
Author: Kannan.G.B <11...@users.noreply.github.com>
AuthorDate: Tue Apr 18 21:58:27 2023 +0530
Yuengling profile table parity (#7434)
* navigation for profile
* profile table for parity CDN-19162
* latest changes
* comment addressed
* duplicate configuration removed, older restored
* config order updated
* e2e test
* lint fix
* added profiles in sidebar object
* container corrected
---
.../traffic-portal/nightwatch/globals/globals.ts | 21 +++
.../nightwatch/page_objects/common.ts | 1 +
.../page_objects/profiles/profilesTable.ts | 46 +++++++
.../nightwatch/tests/profiles/table.spec.ts | 26 ++++
.../traffic-portal/src/app/api/profile.service.ts | 24 +++-
.../src/app/api/testing/profile.service.ts | 36 ++++-
.../traffic-portal/src/app/core/core.module.ts | 3 +
.../profile-table/profile-table.component.html | 27 ++++
.../profile-table/profile-table.component.scss | 13 ++
.../profile-table/profile-table.component.spec.ts | 126 ++++++++++++++++++
.../profile-table/profile-table.component.ts | 148 +++++++++++++++++++++
.../app/shared/navigation/navigation.service.ts | 4 +
12 files changed, 472 insertions(+), 3 deletions(-)
diff --git a/experimental/traffic-portal/nightwatch/globals/globals.ts b/experimental/traffic-portal/nightwatch/globals/globals.ts
index 5db7fdc9a9..afcd947938 100644
--- a/experimental/traffic-portal/nightwatch/globals/globals.ts
+++ b/experimental/traffic-portal/nightwatch/globals/globals.ts
@@ -32,6 +32,7 @@ import type { DeliveryServiceCardPageObject } from "nightwatch/page_objects/deli
import type { DeliveryServiceDetailPageObject } from "nightwatch/page_objects/deliveryServices/deliveryServiceDetail";
import type { DeliveryServiceInvalidPageObject } from "nightwatch/page_objects/deliveryServices/deliveryServiceInvalidationJobs";
import type { LoginPageObject } from "nightwatch/page_objects/login";
+import type { ProfilePageObject } from "nightwatch/page_objects/profiles/profilesTable";
import type { PhysLocDetailPageObject } from "nightwatch/page_objects/servers/physLocDetail";
import type { PhysLocTablePageObject } from "nightwatch/page_objects/servers/physLocTable";
import type { ServersPageObject } from "nightwatch/page_objects/servers/servers";
@@ -65,6 +66,9 @@ import {
ResponseCoordinate,
RequestCoordinate,
RequestType,
+ ResponseProfile,
+ RequestProfile,
+ ProfileType
} from "trafficops-types";
import * as config from "../config.json";
@@ -98,6 +102,9 @@ declare module "nightwatch" {
deliveryServiceInvalidationJobs: () => DeliveryServiceInvalidPageObject;
};
login: () => LoginPageObject;
+ profiles: {
+ profileTable: () => ProfilePageObject;
+ };
servers: {
physLocDetail: () => PhysLocDetailPageObject;
physLocTable: () => PhysLocTablePageObject;
@@ -144,6 +151,7 @@ export interface CreatedData {
steeringDS: ResponseDeliveryService;
tenant: ResponseTenant;
type: TypeFromResponse;
+ profile: ResponseProfile;
}
const testData = {};
@@ -382,6 +390,19 @@ const globals = {
console.log(`Successfully created Type ${respType.name}`);
data.type = respType;
+ const profile: RequestProfile = {
+ cdn: 1,
+ description: "blah",
+ name: `profile${globals.uniqueString}`,
+ routingDisabled: false,
+ type: ProfileType.ATS_PROFILE,
+ };
+ url = `${apiUrl}/profiles`;
+ resp = await client.post(url, JSON.stringify(profile));
+ const respProfile: ResponseProfile = resp.data.response;
+ console.log(`Successfully created Profile ${respProfile.name}`);
+ data.profile = respProfile;
+
} catch(e) {
console.error("Request for", url, "failed:", (e as AxiosError).message);
throw e;
diff --git a/experimental/traffic-portal/nightwatch/page_objects/common.ts b/experimental/traffic-portal/nightwatch/page_objects/common.ts
index 9c4b975a6f..091b647a65 100644
--- a/experimental/traffic-portal/nightwatch/page_objects/common.ts
+++ b/experimental/traffic-portal/nightwatch/page_objects/common.ts
@@ -59,6 +59,7 @@ const commonPageObject = {
otherContainer: "[aria-label='Toggle Other']",
physicalLocations: "[aria-label='Navigate to Physical Locations']",
profile: "[aria-label='Navigate to My Profile']",
+ profiles: "[aria-label='Navigate to Profiles']",
regions: "[aria-label='Navigate to Regions']",
servers: "[aria-label='Navigate to Servers']",
serversContainer: "[aria-label='Toggle Servers']",
diff --git a/experimental/traffic-portal/nightwatch/page_objects/profiles/profilesTable.ts b/experimental/traffic-portal/nightwatch/page_objects/profiles/profilesTable.ts
new file mode 100644
index 0000000000..60e36b9cb9
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/page_objects/profiles/profilesTable.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed 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.
+ */
+
+import { EnhancedPageObject, EnhancedSectionInstance, NightwatchAPI } from "nightwatch";
+
+import { TABLE_COMMANDS, TableSectionCommands } from "../../globals/tables";
+
+/**
+ * Defines the Profiles table commands
+ */
+type ProfileTableCommands = TableSectionCommands;
+
+/**
+ * Defines the Page Object for the Profiles page.
+ */
+export type ProfilePageObject = EnhancedPageObject<{}, {},
+EnhancedSectionInstance<ProfileTableCommands>>;
+
+const profilePageObject = {
+ api: {} as NightwatchAPI,
+ sections: {
+ profileTable: {
+ commands: {
+ ...TABLE_COMMANDS
+ },
+ elements: {},
+ selector: "mat-card"
+ }
+ },
+ url(): string {
+ return `${this.api.launchUrl}/core/profiles`;
+ }
+};
+
+export default profilePageObject;
diff --git a/experimental/traffic-portal/nightwatch/tests/profiles/table.spec.ts b/experimental/traffic-portal/nightwatch/tests/profiles/table.spec.ts
new file mode 100644
index 0000000000..f7a2e78e30
--- /dev/null
+++ b/experimental/traffic-portal/nightwatch/tests/profiles/table.spec.ts
@@ -0,0 +1,26 @@
+/*
+ * Licensed 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.
+ */
+
+describe("Profiles Spec", () => {
+ it("Loads elements", async () => {
+ await browser.page.common()
+ .section.sidebar
+ .navigateToNode("profiles", ["configurationContainer"]);
+ await browser.waitForElementPresent("input[name=fuzzControl]");
+ await browser.elements("css selector", "div.ag-row", rows => {
+ browser.assert.ok(rows.status === 0);
+ browser.assert.ok((rows.value as []).length >= 1);
+ });
+ });
+});
diff --git a/experimental/traffic-portal/src/app/api/profile.service.ts b/experimental/traffic-portal/src/app/api/profile.service.ts
index bf638dbbf1..af494b50c6 100644
--- a/experimental/traffic-portal/src/app/api/profile.service.ts
+++ b/experimental/traffic-portal/src/app/api/profile.service.ts
@@ -14,7 +14,7 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
-import { ResponseProfile } from "trafficops-types";
+import { RequestProfile, ResponseProfile } from "trafficops-types";
import { APIService } from "./base-api.service";
@@ -57,4 +57,26 @@ export class ProfileService extends APIService {
}
return this.get<Array<ResponseProfile>>(path).toPromise();
}
+
+ /**
+ * Creates a new type.
+ *
+ * @param profile The type to create.
+ * @returns The created type.
+ */
+ public async createProfile(profile: RequestProfile): Promise<ResponseProfile> {
+ return this.post<ResponseProfile>("profiles", profile).toPromise();
+ }
+
+ /**
+ * Deletes an existing type.
+ *
+ * @param profileId Id of the profile to delete.
+ * @returns The success message.
+ */
+ public async deleteProfile(profileId: number | ResponseProfile): Promise<ResponseProfile> {
+ const id = typeof (profileId) === "number" ? profileId : profileId.id;
+ return this.delete<ResponseProfile>(`profiles/${id}`).toPromise();
+ }
+
}
diff --git a/experimental/traffic-portal/src/app/api/testing/profile.service.ts b/experimental/traffic-portal/src/app/api/testing/profile.service.ts
index 5f25ad622e..1f4a96b174 100644
--- a/experimental/traffic-portal/src/app/api/testing/profile.service.ts
+++ b/experimental/traffic-portal/src/app/api/testing/profile.service.ts
@@ -13,14 +13,14 @@
*/
import { Injectable } from "@angular/core";
-import { ProfileType, type ResponseProfile } from "trafficops-types";
+import { ProfileType, RequestProfile, type ResponseProfile } from "trafficops-types";
/**
* ProfileService exposes API functionality related to Profiles.
*/
@Injectable()
export class ProfileService {
-
+ private lastID = 10;
private readonly profiles: ResponseProfile[] = [
{
cdn: 1,
@@ -173,4 +173,36 @@ export class ProfileService {
})
);
}
+
+ /**
+ * Creates a new profile.
+ *
+ * @param profile The profile to create.
+ * @returns The created profile.
+ */
+ public async createProfile(profile: RequestProfile): Promise<ResponseProfile> {
+ const t = {
+ ...profile,
+ cdnName: null,
+ id: ++this.lastID,
+ lastUpdated: new Date()
+ };
+ this.profiles.push(t);
+ return t;
+ }
+
+ /**
+ * Deletes an existing profile.
+ *
+ * @param id Id of the profile to delete.
+ * @returns The success message.
+ */
+ public async deleteProfile(id: number): Promise<ResponseProfile> {
+ const index = this.profiles.findIndex(t => t.id === id);
+ if (index === -1) {
+ throw new Error(`no such Type: ${id}`);
+ }
+ return this.profiles.splice(index, 1)[0];
+ }
+
}
diff --git a/experimental/traffic-portal/src/app/core/core.module.ts b/experimental/traffic-portal/src/app/core/core.module.ts
index e4f0aa3123..9828ae033c 100644
--- a/experimental/traffic-portal/src/app/core/core.module.ts
+++ b/experimental/traffic-portal/src/app/core/core.module.ts
@@ -48,6 +48,7 @@ import {
} from "./deliveryservice/invalidation-jobs/new-invalidation-job-dialog/new-invalidation-job-dialog.component";
import { NewDeliveryServiceComponent } from "./deliveryservice/new-delivery-service/new-delivery-service.component";
import { ISOGenerationFormComponent } from "./misc/isogeneration-form/isogeneration-form.component";
+import { ProfileTableComponent } from "./profiles/profile-table/profile-table.component";
import { PhysLocDetailComponent } from "./servers/phys-loc/detail/phys-loc-detail.component";
import { PhysLocTableComponent } from "./servers/phys-loc/table/phys-loc-table.component";
import { ServerDetailsComponent } from "./servers/server-details/server-details.component";
@@ -90,6 +91,7 @@ export const ROUTES: Routes = [
{ component: TypesTableComponent, path: "types" },
{ component: TypeDetailComponent, path: "types/:id"},
{ component: ISOGenerationFormComponent, path: "iso-gen"},
+ { component: ProfileTableComponent, path: "profiles"},
].map(r => ({...r, canActivate: [AuthenticatedGuard]}));
/**
@@ -131,6 +133,7 @@ export const ROUTES: Routes = [
TypesTableComponent,
TypeDetailComponent,
ISOGenerationFormComponent,
+ ProfileTableComponent,
CDNDetailComponent,
],
exports: [],
diff --git a/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.html b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.html
new file mode 100644
index 0000000000..121b7cdcd9
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.html
@@ -0,0 +1,27 @@
+<!--
+Licensed 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.
+-->
+
+<mat-card class="table-page-content">
+ <div class="search-container">
+ <input type="search" name="fuzzControl" aria-label="Fuzzy Search Profiles" autofocus inputmode="search" role="search" accesskey="/" placeholder="Fuzzy Search" [formControl]="fuzzControl" (input)="updateURL()" />
+ </div>
+ <tp-generic-table
+ [data]="profiles | async"
+ [cols]="columnDefs"
+ [fuzzySearch]="fuzzySubject"
+ context="profiles"
+ [contextMenuItems]="contextMenuItems"
+ (contextMenuAction)="handleContextMenu($event)">
+ </tp-generic-table>
+</mat-card>
diff --git a/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.scss b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.scss
new file mode 100644
index 0000000000..ebe77042d3
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.scss
@@ -0,0 +1,13 @@
+/*
+* Licensed 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.
+*/
diff --git a/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.spec.ts b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.spec.ts
new file mode 100644
index 0000000000..53326e993d
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.spec.ts
@@ -0,0 +1,126 @@
+/*
+* Licensed 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.
+*/
+
+import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing";
+import { MatDialog, MatDialogModule, type MatDialogRef } from "@angular/material/dialog";
+import { ActivatedRoute } from "@angular/router";
+import { RouterTestingModule } from "@angular/router/testing";
+import { of } from "rxjs";
+import { ProfileType } from "trafficops-types";
+
+import { ProfileService } from "src/app/api";
+import { APITestingModule } from "src/app/api/testing";
+import { isAction } from "src/app/shared/generic-table/generic-table.component";
+
+import { ProfileTableComponent } from "./profile-table.component";
+
+describe("ProfileTableComponent", () => {
+ let component: ProfileTableComponent;
+ let fixture: ComponentFixture<ProfileTableComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ProfileTableComponent],
+ imports: [
+ APITestingModule,
+ RouterTestingModule,
+ MatDialogModule
+ ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ProfileTableComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ it("sets the fuzzy search subject based on the search query param", fakeAsync(() => {
+ const router = TestBed.inject(ActivatedRoute);
+ const searchString = "testquest";
+ spyOnProperty(router, "queryParamMap").and.returnValue(of(new Map([["search", searchString]])));
+
+ let searchValue = "not the right string";
+ component.fuzzySubject.subscribe(
+ s => searchValue = s
+ );
+
+ component.ngOnInit();
+ tick();
+
+ expect(searchValue).toBe(searchString);
+ }));
+
+ it("updates the fuzzy search output", fakeAsync(() => {
+ let called = false;
+ const text = "testquest";
+ const spy = jasmine.createSpy("subscriber", (txt: string): void => {
+ if (!called) {
+ expect(txt).toBe("");
+ called = true;
+ } else {
+ expect(txt).toBe(text);
+ }
+ });
+ component.fuzzySubject.subscribe(spy);
+ tick();
+ expect(spy).toHaveBeenCalled();
+ component.fuzzControl.setValue(text);
+ component.updateURL();
+ tick();
+ expect(spy).toHaveBeenCalledTimes(2);
+ }));
+
+ it("handles the 'delete' context menu item", fakeAsync(async () => {
+ const item = component.contextMenuItems.find(c => c.name === "Delete");
+ if (!item) {
+ return fail("missing 'Delete' context menu item");
+ }
+ if (!isAction(item)) {
+ return fail("expected an action, not a link");
+ }
+ expect(item.multiRow).toBeFalsy();
+ expect(item.disabled).toBeUndefined();
+
+ const api = TestBed.inject(ProfileService);
+ const spy = spyOn(api, "deleteProfile").and.callThrough();
+ expect(spy).not.toHaveBeenCalled();
+
+ const dialogService = TestBed.inject(MatDialog);
+ const openSpy = spyOn(dialogService, "open").and.returnValue({
+ afterClosed: () => of(true)
+ } as MatDialogRef<unknown>);
+
+ const profile = await api.createProfile({
+ cdn: 1,
+ description: "blah",
+ name: "test",
+ routingDisabled: false,
+ type: ProfileType.ATS_PROFILE
+ });
+ expect(openSpy).not.toHaveBeenCalled();
+ const asyncExpectation = expectAsync(component.handleContextMenu({ action: "delete", data: profile })).toBeResolvedTo(undefined);
+ tick();
+
+ expect(openSpy).toHaveBeenCalled();
+ tick();
+
+ expect(spy).toHaveBeenCalled();
+
+ await asyncExpectation;
+ }));
+});
diff --git a/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.ts b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.ts
new file mode 100644
index 0000000000..075ee04d23
--- /dev/null
+++ b/experimental/traffic-portal/src/app/core/profiles/profile-table/profile-table.component.ts
@@ -0,0 +1,148 @@
+/*
+* Licensed 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.
+*/
+
+import { Component, OnInit } from "@angular/core";
+import { FormControl, UntypedFormControl } from "@angular/forms";
+import { MatDialog } from "@angular/material/dialog";
+import { ActivatedRoute, Params } from "@angular/router";
+import { BehaviorSubject } from "rxjs";
+import { ResponseProfile } from "trafficops-types";
+
+import { ProfileService } from "src/app/api";
+import { CurrentUserService } from "src/app/shared/current-user/current-user.service";
+import { DecisionDialogComponent } from "src/app/shared/dialogs/decision-dialog/decision-dialog.component";
+import { ContextMenuActionEvent, ContextMenuItem } from "src/app/shared/generic-table/generic-table.component";
+import { NavigationService } from "src/app/shared/navigation/navigation.service";
+
+/**
+ * ProfileTableComponent is the controller for the profiles page - which
+ * principally contains a table.
+ */
+@Component({
+ selector: "tp-profile-table",
+ styleUrls: ["./profile-table.component.scss"],
+ templateUrl: "./profile-table.component.html"
+})
+export class ProfileTableComponent implements OnInit {
+ /** All the physical locations which should appear in the table. */
+ public profiles: Promise<Array<ResponseProfile>>;
+
+ /** Definitions of the table's columns according to the ag-grid API */
+ public columnDefs = [{
+ field: "cdnName",
+ headerName: "CDN"
+ }, {
+ field: "description",
+ headerName: "Description",
+ }, {
+ field: "id",
+ headerName: "ID",
+ hide: true
+ }, {
+ field: "lastUpdated",
+ headerName: "Last Updated",
+ hide: true
+ }, {
+ field: "name",
+ headerName: "Name"
+ }, {
+ field: "routingDisabled",
+ headerName: "Routing Disabled"
+ }, {
+ field: "type",
+ headerName: "Type"
+ }];
+
+ /** Definitions for the context menu items (which act on augmented cache-group data). */
+ public contextMenuItems: Array<ContextMenuItem<ResponseProfile>> = [
+ {
+ action: "delete",
+ multiRow: false,
+ name: "Delete"
+ },
+ {
+ href: "/core/servers",
+ name: "View Servers",
+ queryParams: (profile: ResponseProfile): Params => ({profileName: profile.name})
+ }
+ ];
+
+ /** A subject that child components can subscribe to for access to the fuzzy search query text */
+ public fuzzySubject: BehaviorSubject<string>;
+
+ /** Form controller for the user search input. */
+ public fuzzControl: UntypedFormControl = new FormControl<string>("", {nonNullable: true});
+
+ /**
+ * Constructs the component with its required injections.
+ *
+ * @param api The Servers API which is used to provide row data.
+ * @param route A reference to the route of this view which is used to set the fuzzy search box text from the 'search' query parameter.
+ * @param router Angular router
+ * @param navSvc Manages the header
+ * @param dialog Dialog manager
+ */
+ constructor(
+ private readonly api: ProfileService,
+ private readonly route: ActivatedRoute,
+ private readonly navSvc: NavigationService,
+ private readonly dialog: MatDialog,
+ public readonly auth: CurrentUserService) {
+ this.fuzzySubject = new BehaviorSubject<string>("");
+ this.profiles = this.api.getProfiles();
+ this.navSvc.headerTitle.next("Profiles");
+ }
+
+ /** Initializes table data, loading it from Traffic Ops. */
+ public ngOnInit(): void {
+ this.route.queryParamMap.subscribe(
+ m => {
+ const search = m.get("search");
+ if (search) {
+ this.fuzzControl.setValue(decodeURIComponent(search));
+ this.updateURL();
+ }
+ }
+ );
+ }
+
+ /** Update the URL's 'search' query parameter for the user's search input. */
+ public updateURL(): void {
+ this.fuzzySubject.next(this.fuzzControl.value);
+ }
+
+ /**
+ * Handles a context menu event.
+ *
+ * @param evt The action selected from the context menu.
+ */
+ public async handleContextMenu(evt: ContextMenuActionEvent<ResponseProfile>): Promise<void> {
+ const data = evt.data as ResponseProfile;
+ switch (evt.action) {
+ case "delete":
+ const ref = this.dialog.open(DecisionDialogComponent, {
+ data: {
+ message: `Are you sure to delete Profile ${data.name} with id ${data.id}?`,
+ title: "Confirm Delete"
+ }
+ });
+ ref.afterClosed().subscribe(result => {
+ if (result) {
+ this.api.deleteProfile(data.id).then(async () => this.profiles = this.api.getProfiles());
+ }
+ });
+ break;
+ }
+ }
+}
diff --git a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
index b85b2d69e2..14c0dff7aa 100644
--- a/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
+++ b/experimental/traffic-portal/src/app/shared/navigation/navigation.service.ts
@@ -141,6 +141,10 @@ export class NavigationService {
children: [{
href: "/core/types",
name: "Types"
+ },
+ {
+ href: "/core/profiles",
+ name: "Profiles"
}],
name: "Configuration"
}, {