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%;