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 <[email protected]>
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 <[email protected]>
---
 .../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%;

Reply via email to