You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2023/06/22 21:18:58 UTC

[trafficcontrol] branch master updated: Import/Export Profiles in TPv2 (#7532)

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 c1e70b7a7b Import/Export Profiles in TPv2 (#7532)
c1e70b7a7b is described below

commit c1e70b7a7b55b240662b54ca41fc92f760fa94ff
Author: Kannan.G.B <11...@users.noreply.github.com>
AuthorDate: Fri Jun 23 02:48:51 2023 +0530

    Import/Export Profiles in TPv2 (#7532)
    
    * import-export profile
    
    * testing and code fixes for pr
    
    * test case added for import profile
    
    * optional removed, version changed
    
    * revert to old code
    
    * proper fileutilservice import
    
    * removed editable textarea
    
    * removed seperate export attachment service
    
    * profile export fnction added with test
    
    * comments addresed
    
    * lint fixes
    
    * lint issue fixes
    
    * comment changes
    
    * review comments addressed
    
    * ui issue fix
    
    * comments fixed
    
    * comment addressed
    
    * comment updated
    
    * Add component storage of file list
    
    * rework existing methods to utilize stored file
    
    * Grammar, linking, spelling etc. updates in comments and user messages
    
    Also made things that aren't meant to be changed readonly
    
    * Eliminate unused button
    
    * rework DOM structure
    
    includes changing an element reference and an ID, also now sets accept
    from controller property instead of duplicating values
    
    * Bind files to input
    
    * Use hidden instead of ngIf
    
    Less jumping around of the page elements when conditions change that
    way. I also made the conditional bindings a bit more sensible, both in
    target and condition.
    
    * Accessibility best practices changes
    
    * Rework styling to be compliant with WCAG
    
    ... and also to look better, more like it used to before I changed
    anything.
    
    * Fix button type
    
    * added cursor
    
    ---------
    
    Co-authored-by: ocket8888 <oc...@apache.org>
---
 .../src/app/api/profile.service.spec.ts            |  41 ++++-
 .../traffic-portal/src/app/api/profile.service.ts  |  26 ++-
 .../src/app/api/testing/profile.service.ts         |  46 +++++-
 .../profile-table/profile-table.component.html     |   2 +
 .../profile-table/profile-table.component.spec.ts  |   4 +
 .../profile-table/profile-table.component.ts       |  54 ++++++-
 .../import-json-txt/import-json-txt.component.html |  47 ++++++
 .../import-json-txt/import-json-txt.component.scss |  42 +++++
 .../import-json-txt.component.spec.ts              |  73 +++++++++
 .../import-json-txt/import-json-txt.component.ts   | 178 +++++++++++++++++++++
 .../app/shared/interceptor/alerts.interceptor.ts   |   3 +-
 .../traffic-portal/src/app/shared/shared.module.ts |   7 +-
 experimental/traffic-portal/src/styles.scss        |   4 +-
 13 files changed, 510 insertions(+), 17 deletions(-)

diff --git a/experimental/traffic-portal/src/app/api/profile.service.spec.ts b/experimental/traffic-portal/src/app/api/profile.service.spec.ts
index 17d0b35ba3..63ce8b22f3 100644
--- a/experimental/traffic-portal/src/app/api/profile.service.spec.ts
+++ b/experimental/traffic-portal/src/app/api/profile.service.spec.ts
@@ -14,7 +14,7 @@
  */
 import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
 import { TestBed } from "@angular/core/testing";
-import {ProfileType, ResponseProfile} from "trafficops-types";
+import { ProfileExport, ProfileType , ResponseProfile} from "trafficops-types";
 
 import { ProfileService } from "./profile.service";
 
@@ -31,6 +31,26 @@ describe("ProfileService", () => {
 		routingDisabled: false,
 		type: ProfileType.ATS_PROFILE
 	};
+	const importProfile = {
+		parameters:[],
+		profile: {
+			cdn: "CDN",
+			description: "",
+			id: 1,
+			name: "TestQuest",
+			type: ProfileType.ATS_PROFILE,
+		}
+	};
+	const exportProfile: ProfileExport = {
+		alerts: null,
+		parameters:[],
+		profile: {
+			cdn: "ALL",
+			description: "test",
+			name: "TRAFFIC_ANALYTICS",
+			type: ProfileType.TS_PROFILE
+		}
+	};
 
 	const parameter = {
 		configFile: "cfg.txt",
@@ -116,6 +136,25 @@ describe("ProfileService", () => {
 		await expectAsync(responseP).toBeResolvedTo(profile);
 	});
 
+	it("sends request for Export object by Profile ID", async () => {
+		const response = service.exportProfile(profile.id);
+		const req = httpTestingController.expectOne(r => r.url === `/api/${service.apiVersion}/profiles/${profile.id}/export`);
+		expect(req.request.method).toBe("GET");
+		expect(req.request.params.keys().length).toBe(0);
+		req.flush(exportProfile);
+		await expectAsync(response).toBeResolvedTo(exportProfile);
+	});
+
+	it("send request for import profile", async () => {
+		const responseP = service.importProfile(importProfile);
+		const req = httpTestingController.expectOne(`/api/${service.apiVersion}/profiles/import`);
+		expect(req.request.method).toBe("POST");
+		expect(req.request.params.keys().length).toBe(0);
+		expect(req.request.body).toBe(importProfile);
+		req.flush({response: importProfile.profile});
+		await expectAsync(responseP).toBeResolvedTo(importProfile.profile);
+	});
+
 	it("sends requests multiple Parameters", async () => {
 		const responseParams = service.getParameters();
 		const req = httpTestingController.expectOne(`/api/${service.apiVersion}/parameters`);
diff --git a/experimental/traffic-portal/src/app/api/profile.service.ts b/experimental/traffic-portal/src/app/api/profile.service.ts
index c628017135..b6920a8cfa 100644
--- a/experimental/traffic-portal/src/app/api/profile.service.ts
+++ b/experimental/traffic-portal/src/app/api/profile.service.ts
@@ -14,7 +14,10 @@
 
 import { HttpClient } from "@angular/common/http";
 import { Injectable } from "@angular/core";
-import {RequestParameter, RequestProfile, ResponseParameter, ResponseProfile} from "trafficops-types";
+import {
+	ProfileExport, ProfileImport, ProfileImportResponse, RequestParameter,
+	RequestProfile, ResponseParameter, ResponseProfile
+} from "trafficops-types";
 
 import { APIService } from "./base-api.service";
 
@@ -124,6 +127,27 @@ export class ProfileService extends APIService {
 		return this.delete<ResponseProfile>(`profiles/${id}`).toPromise();
 	}
 
+	/**
+	 * Exports profile
+	 *
+	 * @param profileId Id of the profile to export.
+	 * @returns profile export object.
+	 */
+	public async exportProfile(profileId: number | ResponseProfile): Promise<ProfileExport>{
+		const id = typeof (profileId) === "number" ? profileId : profileId.id;
+		return this.http.get<ProfileExport>(`/api/${this.apiVersion}/profiles/${id}/export`).toPromise();
+	}
+
+	/**
+	 * Import profile
+	 *
+	 * @param importJSON JSON object for import.
+	 * @returns profile response for imported object.
+	 */
+	public async importProfile(importJSON: ProfileImport): Promise<ProfileImportResponse>{
+		return this.post<ProfileImportResponse>("profiles/import", importJSON).toPromise();
+	}
+
 	public async getParameters(id: number): Promise<ResponseParameter>;
 	public async getParameters(): Promise<Array<ResponseParameter>>;
 	/**
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 ead9c178b1..4a0440850d 100644
--- a/experimental/traffic-portal/src/app/api/testing/profile.service.ts
+++ b/experimental/traffic-portal/src/app/api/testing/profile.service.ts
@@ -13,7 +13,10 @@
 */
 
 import { Injectable } from "@angular/core";
-import {ProfileType, RequestParameter, RequestProfile, ResponseParameter, ResponseProfile} from "trafficops-types";
+import {
+	ProfileExport, ProfileImport, ProfileImportResponse, ProfileType,
+	RequestProfile, ResponseProfile, RequestParameter, ResponseParameter
+} from "trafficops-types";
 
 /**
  * ProfileService exposes API functionality related to Profiles.
@@ -135,6 +138,16 @@ export class ProfileService {
 			type: ProfileType.ATS_PROFILE
 		}
 	];
+	private readonly profileExport: ProfileExport = {
+		alerts: null,
+		parameters:[],
+		profile: {
+			cdn: "ALL",
+			description: "test",
+			name: "TRAFFIC_ANALYTICS",
+			type: ProfileType.TS_PROFILE
+		},
+	};
 
 	public async getProfiles(idOrName: number | string): Promise<ResponseProfile>;
 	public async getProfiles(): Promise<Array<ResponseProfile>>;
@@ -215,11 +228,40 @@ export class ProfileService {
 	public async deleteProfile(id: number | ResponseProfile): Promise<ResponseProfile> {
 		const index = this.profiles.findIndex(t => t.id === id);
 		if (index === -1) {
-			throw new Error(`no such Type: ${id}`);
+			throw new Error(`no such profile: ${id}`);
 		}
 		return this.profiles.splice(index, 1)[0];
 	}
 
+	/**
+	 * Export Profile object from the API.
+	 *
+	 * @param profile Specify unique identifier (number) of a specific Profile to retrieve the export object.
+	 * @returns The requested Profile as attachment.
+	 */
+	public async exportProfile(profile: number | ResponseProfile): Promise<ProfileExport> {
+		const id = typeof(profile) === "number" ? profile : profile.id;
+		const index = this.profiles.findIndex(t => t.id === id);
+		if (index === -1) {
+			throw new Error(`no such Profile: ${id}`);
+		}
+		return this.profileExport;
+	}
+
+	/**
+	 * import profile from json or text file
+	 *
+	 * @param profile imported date for profile creation.
+	 * @returns The created profile which is profileImportResponse with id added.
+	 */
+	public async importProfile(profile: ProfileImport): Promise<ProfileImportResponse> {
+		const t = {
+			...profile.profile,
+			id: ++this.lastID,
+		};
+		return t;
+	}
+
 	private lastParamID = 20;
 	private readonly parameters:  ResponseParameter[] = [
 		{
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
index 2cd0ba90b4..21902a62f8 100644
--- 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
@@ -21,6 +21,8 @@ limitations under the License.
 		[cols]="columnDefs"
 		[fuzzySearch]="fuzzySubject"
 		context="profiles"
+		[tableTitleButtons]="titleBtns"
+		(tableTitleButtonAction)="handleTitleButton($event)"
 		[contextMenuItems]="contextMenuItems"
 		(contextMenuAction)="handleContextMenu($event)">
 	</tp-generic-table>
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
index 739fdccfc5..ed4bfa266a 100644
--- 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
@@ -21,6 +21,7 @@ import { ProfileType } from "trafficops-types";
 
 import { ProfileService } from "src/app/api";
 import { APITestingModule } from "src/app/api/testing";
+import { FileUtilsService } from "src/app/shared/file-utils.service";
 import { isAction } from "src/app/shared/generic-table/generic-table.component";
 
 import { ProfileTableComponent } from "./profile-table.component";
@@ -36,6 +37,9 @@ describe("ProfileTableComponent", () => {
 				APITestingModule,
 				RouterTestingModule,
 				MatDialogModule
+			],
+			providers:[
+				FileUtilsService
 			]
 		})
 			.compileComponents();
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
index 742645c0a5..9fd7f5171c 100644
--- 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
@@ -17,12 +17,14 @@ 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 { ProfileImport, 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 { FileUtilsService } from "src/app/shared/file-utils.service";
+import { ContextMenuActionEvent, ContextMenuItem, TableTitleButton } from "src/app/shared/generic-table/generic-table.component";
+import { ImportJsonTxtComponent } from "src/app/shared/import-json-txt/import-json-txt.component";
 import { NavigationService } from "src/app/shared/navigation/navigation.service";
 
 /**
@@ -32,7 +34,7 @@ import { NavigationService } from "src/app/shared/navigation/navigation.service"
 @Component({
 	selector: "tp-profile-table",
 	styleUrls: ["./profile-table.component.scss"],
-	templateUrl: "./profile-table.component.html"
+	templateUrl: "./profile-table.component.html",
 })
 export class ProfileTableComponent implements OnInit {
 	/** All the physical locations which should appear in the table. */
@@ -64,6 +66,13 @@ export class ProfileTableComponent implements OnInit {
 		headerName: "Type"
 	}];
 
+	public titleBtns: Array<TableTitleButton> = [
+		{
+			action: "import",
+			text: "Import Profile",
+		}
+	];
+
 	/** Definitions for the context menu items (which act on augmented cache-group data). */
 	public contextMenuItems: Array<ContextMenuItem<ResponseProfile>> = [
 		{
@@ -81,14 +90,13 @@ export class ProfileTableComponent implements OnInit {
 			name: "Delete"
 		},
 		{
-			action: "import-profile",
+			action: "clone-profile",
 			disabled: (): true => true,
 			multiRow: false,
-			name: "Import Profile",
+			name: "Clone Profile",
 		},
 		{
 			action: "export-profile",
-			disabled: (): true => true,
 			multiRow: false,
 			name: "Export Profile",
 		},
@@ -125,7 +133,8 @@ export class ProfileTableComponent implements OnInit {
 		private readonly route: ActivatedRoute,
 		private readonly navSvc: NavigationService,
 		private readonly dialog: MatDialog,
-		public readonly auth: CurrentUserService) {
+		public readonly auth: CurrentUserService,
+		private readonly fileUtil: FileUtilsService) {
 		this.fuzzySubject = new BehaviorSubject<string>("");
 		this.profiles = this.api.getProfiles();
 		this.navSvc.headerTitle.next("Profiles");
@@ -175,6 +184,37 @@ export class ProfileTableComponent implements OnInit {
 					}
 				});
 				break;
+			case "export-profile":
+				const response = await this.api.exportProfile(data.id);
+				this.fileUtil.download(response,data.name);
+				break;
+		}
+	}
+
+	/**
+	 * handles when a title button is event is emitted
+	 *
+	 * @param action which button was pressed
+	 */
+	public async handleTitleButton(action: string): Promise<void> {
+		switch(action){
+			case "import":
+				const ref = this.dialog.open(ImportJsonTxtComponent,{
+					data: { title: "Import Profile" },
+					width: "70vw"
+				});
+
+				/** After submission from Import JSON dialog component */
+				ref.afterClosed().subscribe( (result: ProfileImport) => {
+					if (result) {
+						this.api.importProfile(result).then(response => {
+							if (response) {
+								this.profiles = this.api.getProfiles();
+							}
+						});
+					}
+				});
+				break;
 		}
 	}
 }
diff --git a/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.html b/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.html
new file mode 100644
index 0000000000..e9c9c00b4b
--- /dev/null
+++ b/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.html
@@ -0,0 +1,47 @@
+<!--
+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.
+-->
+
+<h2 mat-dialog-title>{{data.title}}</h2>
+<mat-dialog-content id="import-content" [ngClass]="{'active':dragOn}">
+	<div class="dropzone">
+		<label #inputLabel
+			for="profile-upload"
+			tabindex="0"
+			(keydown.enter)="inputLabel.click()"
+			(keydown.space)="inputLabel.click()"
+		>Click or Drop your file here to upload</label>
+		<input
+			type="file"
+			id="profile-upload"
+			[accept]="allowedType.join(', ')"
+			[files]="files"
+			(change)="uploadFile($event)"
+			hidden
+			aria-describedby="file-name json-txt"
+		/>
+		<small class="hint">{{mimeAlertMsg}}</small>
+	</div>
+
+	<ul [hidden]="!fileData">
+		<li id="file-name">{{fileData}}</li>
+	</ul>
+
+	<div id="json-txt" [hidden]="files.length !== 1">
+		<pre>{{inputTxt | json}}</pre>
+	</div>
+</mat-dialog-content>
+<mat-dialog-actions align="end">
+	<button mat-raised-button type="button" mat-dialog-close color="warn">Cancel</button>
+	<button mat-raised-button type="button" [mat-dialog-close]="inputTxt" [disabled]="!file">Submit</button>
+</mat-dialog-actions>
diff --git a/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.scss b/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.scss
new file mode 100644
index 0000000000..d888fbb3ce
--- /dev/null
+++ b/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.scss
@@ -0,0 +1,42 @@
+/*
+* 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.
+*/
+
+$backgroundColor: #fafaec;
+
+.dropzone {
+	max-width: 80vw;
+	padding: 2rem;
+	margin: 1rem auto;
+	background-color: $backgroundColor;
+	border: solid 1px rgb(231, 230, 230);
+	text-align: center;
+	color: black;
+	border-radius: 5px;
+	box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075);
+	cursor: pointer;
+	
+    label {
+		display: block;
+	}
+	.hint {
+		opacity: 0.87;
+		display: block;
+	}
+}
+
+#import-content {
+	&.active {
+		background: $backgroundColor;
+	}
+}
diff --git a/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.spec.ts b/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.spec.ts
new file mode 100644
index 0000000000..5ca07e7eb1
--- /dev/null
+++ b/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.spec.ts
@@ -0,0 +1,73 @@
+/*
+* 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 { DatePipe } from "@angular/common";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog";
+
+import { ImportJsonTxtComponent } from "./import-json-txt.component";
+
+describe("ImportJsonTxtComponent", () => {
+	let component: ImportJsonTxtComponent;
+	let fixture: ComponentFixture<ImportJsonTxtComponent>;
+
+	beforeEach(async () => {
+		await TestBed.configureTestingModule({
+			declarations: [ ImportJsonTxtComponent ],
+			imports: [
+				MatDialogModule
+			],
+			providers: [
+				DatePipe,
+				{provide: MatDialogRef, useValue: {}},
+				{provide: MAT_DIALOG_DATA, useValue: { title: ""}}
+			]
+		}).compileComponents();
+
+		fixture = TestBed.createComponent(ImportJsonTxtComponent);
+		component = fixture.componentInstance;
+		fixture.detectChanges();
+	});
+
+	it("should create", () => {
+		expect(component).toBeTruthy();
+	});
+
+	it("should set dragOn to true when dragover event occurs", () => {
+		const event = new DragEvent("dragover");
+		const preventDefaultSpy = spyOn(event, "preventDefault");
+		const stopPropagationSpy = spyOn(event, "stopPropagation");
+
+		fixture.nativeElement.dispatchEvent(event);
+		fixture.detectChanges();
+
+		expect(preventDefaultSpy).toHaveBeenCalled();
+		expect(stopPropagationSpy).toHaveBeenCalled();
+		expect(component.dragOn).toBeTrue();
+	  });
+
+	  it("should set dragOn to true when dragover event occurs", () => {
+
+		const event = new DragEvent("dragleave");
+		const preventDefaultSpy = spyOn(event, "preventDefault");
+		const stopPropagationSpy = spyOn(event, "stopPropagation");
+
+		fixture.nativeElement.dispatchEvent(event);
+		fixture.detectChanges();
+
+		expect(preventDefaultSpy).toHaveBeenCalled();
+		expect(stopPropagationSpy).toHaveBeenCalled();
+		expect(component.dragOn).toBeFalse();
+	  });
+});
diff --git a/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.ts b/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.ts
new file mode 100644
index 0000000000..68afebe0a5
--- /dev/null
+++ b/experimental/traffic-portal/src/app/shared/import-json-txt/import-json-txt.component.ts
@@ -0,0 +1,178 @@
+/*
+* 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 { DatePipe } from "@angular/common";
+import { Component, HostListener, Inject } from "@angular/core";
+import { MAT_DIALOG_DATA } from "@angular/material/dialog";
+import { AlertLevel } from "trafficops-types";
+
+import { AlertService } from "../alert/alert.service";
+
+/**
+ * Contains the structure of the data that {@link ImportJsonTxtComponent}
+ * accepts.
+ */
+export interface ImportJsonTxtComponentModel {
+	title: string;
+}
+
+/**
+ * Component for import of JSON or text files.
+ */
+@Component({
+	selector: "tp-import-json-txt",
+	styleUrls: ["./import-json-txt.component.scss"],
+	templateUrl: "./import-json-txt.component.html",
+})
+export class ImportJsonTxtComponent {
+
+	/**
+	 * Allowed import file types.
+	 */
+	public readonly allowedType: readonly string[] = ["application/json", "text/plain"];
+
+	public file: File | null = null;
+
+	/** Text editor value */
+	public inputTxt = "";
+
+	/**  File data imported */
+	public fileData = "";
+
+	/** Monitor whether any file is being drag over the dialog */
+	public dragOn = false;
+
+	public readonly mimeAlertMsg = "Only JSON or text files are allowed.";
+
+	/**
+	 * The value of the file input is maintained by extracting drag-and-drop
+	 * files and setting the input's value accordingly. Note that when setting
+	 * this property, all but the first file are discarded, as it assumes that
+	 * multiple selection is not allowed.
+	 */
+	public get files(): FileList {
+		const dt = new DataTransfer();
+		if (this.file) {
+			dt.items.add(this.file);
+		}
+		return dt.files;
+	}
+
+	public set files(fl: FileList) {
+		this.file = fl[0] ?? null;
+	}
+
+	/**
+	 * Creates an instance of import json edit txt component.
+	 *
+	 * @param dialogRef Dialog manager
+	 * @param alertService Alert service manager
+	 * @param datePipe Default angular date pipe for formating date
+	 */
+	constructor(
+		@Inject(MAT_DIALOG_DATA) public readonly data: ImportJsonTxtComponentModel,
+		private readonly alertService: AlertService,
+		private readonly datePipe: DatePipe) { }
+
+	/**
+	 * Hosts listener for drag over
+	 *
+	 * @param evt Drag events data
+	 */
+	@HostListener("dragover", ["$event"]) public onDragOver(evt: DragEvent): void {
+		evt.preventDefault();
+		evt.stopPropagation();
+
+		this.dragOn = true;
+	}
+
+	/**
+	 * Hosts listener for drag leave
+	 *
+	 * @param evt Drag events data
+	 */
+	@HostListener("dragleave", ["$event"]) public onDragLeave(evt: DragEvent): void {
+		evt.preventDefault();
+		evt.stopPropagation();
+
+		this.dragOn = false;
+	}
+
+	/**
+	 * Hosts listener for drop
+	 *
+	 * @param evt Drag events data
+	 */
+	@HostListener("drop", ["$event"]) public onDrop(evt: DragEvent): void {
+		evt.preventDefault();
+		evt.stopPropagation();
+
+		this.dragOn = false;
+		if (!evt.dataTransfer) {
+			return;
+		}
+
+		this.files = evt.dataTransfer.files;
+		this.docReader();
+	}
+
+	/**
+	 * Uploads file
+	 *
+	 * @param event Event object for upload file
+	 */
+	public uploadFile(event: Event): void {
+		if (!(event.target instanceof HTMLInputElement) || !event.target.files) {
+			console.warn("file uploading triggered on non-file-input element:", event.target);
+			return;
+		}
+
+		this.files = event.target.files;
+		this.docReader();
+	  }
+
+	/**
+	 * Docs reader
+	 *
+	 * @param file that is uploaded
+	 */
+	private docReader(): void {
+		if (!this.file) {
+			return;
+		}
+
+		/**
+		 * Check whether expected file is being uploaded
+		 * returns on file wrong file type is uploaded
+		 */
+		if (!this.allowedType.includes(this.file.type)) {
+			this.alertService.newAlert({ level: AlertLevel.ERROR, text: this.mimeAlertMsg });
+			return;
+		}
+
+		/** Format text with data from file data and formated date with date pipe */
+		const dateStr = this.datePipe.transform(this.file.lastModified, "MM-dd-yyyy");
+		this.fileData = `${this.file.name} - ${this.file.size} bytes, last modified: ${dateStr}`;
+
+		const reader = new FileReader();
+		reader.addEventListener("load",
+			event => {
+				if(typeof(event.target?.result)==="string"){
+					this.inputTxt = JSON.parse(event.target.result);
+				}
+			}
+		);
+		reader.readAsText(this.file);
+	}
+}
diff --git a/experimental/traffic-portal/src/app/shared/interceptor/alerts.interceptor.ts b/experimental/traffic-portal/src/app/shared/interceptor/alerts.interceptor.ts
index b17cc0108a..2ea1d3047e 100644
--- a/experimental/traffic-portal/src/app/shared/interceptor/alerts.interceptor.ts
+++ b/experimental/traffic-portal/src/app/shared/interceptor/alerts.interceptor.ts
@@ -42,7 +42,8 @@ export class AlertInterceptor implements HttpInterceptor {
 		return next.handle(request).pipe(tap(
 			r => {
 				if (Object.prototype.hasOwnProperty.call(r, "body") &&
-					Object.prototype.hasOwnProperty.call((r as { body: unknown }).body, "alerts")) {
+					Object.prototype.hasOwnProperty.call((r as { body: unknown }).body, "alerts")  &&
+					(r as {body: {alerts: Array<unknown>}}).body.alerts !== null) { //Ignore alerts with null value) {
 					for (const a of (r as { body: { alerts: Array<unknown> } }).body.alerts) {
 						this.alertService.newAlert(a as Alert);
 					}
diff --git a/experimental/traffic-portal/src/app/shared/shared.module.ts b/experimental/traffic-portal/src/app/shared/shared.module.ts
index d6a96247e4..2aeac307ce 100644
--- a/experimental/traffic-portal/src/app/shared/shared.module.ts
+++ b/experimental/traffic-portal/src/app/shared/shared.module.ts
@@ -11,7 +11,7 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
-import { CommonModule } from "@angular/common";
+import { CommonModule, DatePipe } from "@angular/common";
 import { HTTP_INTERCEPTORS } from "@angular/common/http";
 import { NgModule } from "@angular/core";
 import { RouterModule } from "@angular/router";
@@ -27,6 +27,7 @@ import { DecisionDialogComponent } from "./dialogs/decision-dialog/decision-dial
 import { TextDialogComponent } from "./dialogs/text-dialog/text-dialog.component";
 import { FileUtilsService } from "./file-utils.service";
 import { GenericTableComponent } from "./generic-table/generic-table.component";
+import { ImportJsonTxtComponent } from "./import-json-txt/import-json-txt.component";
 import { AlertInterceptor } from "./interceptor/alerts.interceptor";
 import { DateReviverInterceptor } from "./interceptor/date-reviver.interceptor";
 import { ErrorInterceptor } from "./interceptor/error.interceptor";
@@ -61,7 +62,8 @@ import { CustomvalidityDirective } from "./validation/customvalidity.directive";
 		TreeSelectComponent,
 		TextDialogComponent,
 		DecisionDialogComponent,
-		CollectionChoiceDialogComponent
+		CollectionChoiceDialogComponent,
+  		ImportJsonTxtComponent
 	],
 	exports: [
 		AlertComponent,
@@ -86,6 +88,7 @@ import { CustomvalidityDirective } from "./validation/customvalidity.directive";
 		{ multi: true, provide: HTTP_INTERCEPTORS, useClass: AlertInterceptor },
 		{ multi: true, provide: HTTP_INTERCEPTORS, useClass: DateReviverInterceptor },
 		FileUtilsService,
+		DatePipe
 	]
 })
 export class SharedModule { }
diff --git a/experimental/traffic-portal/src/styles.scss b/experimental/traffic-portal/src/styles.scss
index ea5af5ebbe..16c8852780 100644
--- a/experimental/traffic-portal/src/styles.scss
+++ b/experimental/traffic-portal/src/styles.scss
@@ -227,9 +227,7 @@ button {
 }
 
 .table-page-content {
-	width: fit-content;
-	min-width: 50%;
-	margin: 1em auto;
+	margin: 1em;
 
 	& > div.search-container {
 		width: 50%;