Skip to content

Commit

Permalink
Import/Export Profiles in TPv2 (#7532)
Browse files Browse the repository at this point in the history
* 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 <ocket8888@apache.org>
  • Loading branch information
gbkannan89 and ocket8888 committed Jun 22, 2023
1 parent 878cd26 commit c1e70b7
Show file tree
Hide file tree
Showing 13 changed files with 510 additions and 17 deletions.
41 changes: 40 additions & 1 deletion experimental/traffic-portal/src/app/api/profile.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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",
Expand Down Expand Up @@ -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`);
Expand Down
26 changes: 25 additions & 1 deletion experimental/traffic-portal/src/app/api/profile.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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>>;
/**
Expand Down
46 changes: 44 additions & 2 deletions experimental/traffic-portal/src/app/api/testing/profile.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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>>;
Expand Down Expand Up @@ -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[] = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
[cols]="columnDefs"
[fuzzySearch]="fuzzySubject"
context="profiles"
[tableTitleButtons]="titleBtns"
(tableTitleButtonAction)="handleTitleButton($event)"
[contextMenuItems]="contextMenuItems"
(contextMenuAction)="handleContextMenu($event)">
</tp-generic-table>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -36,6 +37,9 @@ describe("ProfileTableComponent", () => {
APITestingModule,
RouterTestingModule,
MatDialogModule
],
providers:[
FileUtilsService
]
})
.compileComponents();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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. */
Expand Down Expand Up @@ -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>> = [
{
Expand All @@ -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",
},
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit c1e70b7

Please sign in to comment.