Fixes after review

This commit is contained in:
PiotrP
2018-10-26 05:35:02 -07:00
parent 4084fb39e0
commit 668235936d
5 changed files with 131 additions and 44 deletions

View File

@ -1,7 +1,7 @@
import * as Raven from 'raven-js'; import * as Raven from 'raven-js';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgModule, ErrorHandler } from '@angular/core'; import { NgModule, ErrorHandler } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CdkTableModule } from "@angular/cdk/table"; import { CdkTableModule } from "@angular/cdk/table";
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
@ -131,6 +131,7 @@ if (environment.production) {
HttpClientModule, HttpClientModule,
AppRoutingModule, AppRoutingModule,
FormsModule, FormsModule,
ReactiveFormsModule,
BrowserAnimationsModule, BrowserAnimationsModule,
CdkTableModule, CdkTableModule,
MatButtonModule, MatButtonModule,

View File

@ -1,9 +1,33 @@
.non-visible {
display: none;
}
.file-button{
height: 50px;
width: 120px;
margin-top: 10px;
}
.file-name-form { .file-name-form {
float: right;
}
.file-name-form-field {
margin-left: 10px; margin-left: 10px;
margin-right: 10px; margin-right: 10px;
width:250px; width:250px;
} }
.non-visible { .delete-button {
display: none; background: transparent;
border: none;
outline: 0
}
.delete-icon {
vertical-align: "middle";
}
.result-message-box {
margin-top: 10px;
} }

View File

@ -1,26 +1,34 @@
<h1 mat-dialog-title>Import project</h1> <h1 mat-dialog-title>Import project</h1>
<mat-horizontal-stepper #stepper [linear]="true"> <mat-horizontal-stepper #stepper [linear]="true">
<mat-step label="Choose file" editable="false" completed="false"> <mat-step label="Choose file" editable="false" completed="false">
<div>
<input type="file" accept=".gns3project, .gns3p" class="non-visible" #file (change)="uploadProjectFile($event)" ng2FileSelect [uploader]="uploader"/> <input type="file" accept=".gns3project, .gns3p" class="non-visible" #file (change)="uploadProjectFile($event)" ng2FileSelect [uploader]="uploader"/>
<button mat-raised-button color="primary" (click)="file.click()">Choose file</button> <button mat-raised-button color="primary" (click)="file.click()" class="file-button">Choose file</button>
<mat-form-field class="file-name-form"> <form [formGroup]="projectNameForm" class="file-name-form">
<input matInput tabindex="1" (input)="validateInput()" [(ngModel)]="projectName" placeholder="Please enter name"> <mat-form-field class="file-name-form-field">
<input matInput type="text" formControlName="projectName" [ngClass]="{ 'is-invalid': form.projectName.errors }" placeholder="Please enter name" />
<mat-error *ngIf="form.projectName.errors && form.projectName.errors.required">Project name is required</mat-error>
<mat-error *ngIf="form.projectName.errors && form.projectName.errors.invalidName">Project name is incorrect</mat-error>
</mat-form-field> </mat-form-field>
<button class="material-icons" (click)="onDeleteClick()">delete</button> <button class="delete-button">
<mat-icon color="primary" (click)="onDeleteClick()" class="delete-icon">clear</mat-icon>
</button>
<div class="buttons-bar"> <div class="buttons-bar">
<button mat-button (click)="onNoClick()" color="accent">Cancel</button> <button mat-button (click)="onNoClick()" color="accent">Cancel</button>
<button mat-button ng-disabled="!isImportEnabled" (click)="onImportClick()" tabindex="2" mat-raised-button color="primary">Import</button> <button mat-button [disabled]="!isImportEnabled" (click)="onImportClick()" tabindex="2" mat-raised-button color="primary">Import</button>
</div>
</form>
</div> </div>
</mat-step> </mat-step>
<mat-step label="Progress" editable="false" completed="false"> <mat-step label="Progress" editable="false" completed="false">
<div class="progress"> <div class="progress">
<div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': uploader.progress + '%' }"></div> <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': uploader.progress + '%' }"></div>
</div> </div>
<div> <div class="result-message-box">
<span>{{errorMessage}}</span> <span>{{resultMessage}}</span>
</div> </div>
<div class="buttons-bar"> <div class="buttons-bar">
<button mat-button ng-disabled="!isFinishEnabled" (click)="onNoClick()" tabindex="2" mat-raised-button color="primary">Finish</button> <button mat-button [disabled]="!isFinishEnabled" (click)="onNoClick()" tabindex="2" mat-raised-button color="primary">Finish</button>
</div> </div>
</mat-step> </mat-step>
</mat-horizontal-stepper> </mat-horizontal-stepper>

View File

@ -1,11 +1,11 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ImportProjectDialogComponent } from "./import-project-dialog.component"; import { ImportProjectDialogComponent, Validator } from "./import-project-dialog.component";
import { Server } from "../../../models/server"; import { Server } from "../../../models/server";
import { MatInputModule, MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule, MatStepperModule, MatFormFieldModule, MatDialogRef, MatDialog, MAT_DIALOG_DATA } from "@angular/material"; import { MatInputModule, MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule, MatStepperModule, MatFormFieldModule, MatDialogRef, MatDialog, MAT_DIALOG_DATA } from "@angular/material";
import { RouterTestingModule } from "@angular/router/testing"; import { RouterTestingModule } from "@angular/router/testing";
import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { FileUploadModule, FileSelectDirective, FileItem, FileUploader } from "ng2-file-upload"; import { FileUploadModule, FileSelectDirective, FileItem, FileUploader } from "ng2-file-upload";
import { FormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule, FormBuilder, FormControl, Validators } from '@angular/forms';
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
@ -13,9 +13,9 @@ describe('ImportProjectDialogComponent', () => {
let component: ImportProjectDialogComponent; let component: ImportProjectDialogComponent;
let fixture: ComponentFixture<ImportProjectDialogComponent>; let fixture: ComponentFixture<ImportProjectDialogComponent>;
let server: Server; let server: Server;
let dialog: MatDialog;
let debugElement: DebugElement; let debugElement: DebugElement;
let fileSelectDirective: FileSelectDirective; let fileSelectDirective: FileSelectDirective;
let formBuilder: FormBuilder;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -31,6 +31,7 @@ describe('ImportProjectDialogComponent', () => {
NoopAnimationsModule, NoopAnimationsModule,
FileUploadModule, FileUploadModule,
FormsModule, FormsModule,
ReactiveFormsModule,
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
], ],
providers: [ providers: [
@ -41,10 +42,10 @@ describe('ImportProjectDialogComponent', () => {
}) })
.compileComponents(); .compileComponents();
dialog = TestBed.get(MatDialog);
server = new Server(); server = new Server();
server.ip = "localhost"; server.ip = "localhost";
server.port = 80; server.port = 80;
formBuilder = new FormBuilder();
})); }));
beforeEach(() => { beforeEach(() => {
@ -52,7 +53,10 @@ describe('ImportProjectDialogComponent', () => {
debugElement = fixture.debugElement; debugElement = fixture.debugElement;
component = fixture.componentInstance; component = fixture.componentInstance;
component.server = server; component.server = server;
component.projectNameForm = formBuilder.group({
projectName: new FormControl(null, [Validators.required, Validator.projectNameValidator])
});
component.projectNameForm.controls['projectName'].setValue("ValidName");
fixture.detectChanges(); fixture.detectChanges();
debugElement = fixture.debugElement.query(By.directive(FileSelectDirective)); debugElement = fixture.debugElement.query(By.directive(FileSelectDirective));
@ -119,9 +123,9 @@ describe('ImportProjectDialogComponent', () => {
it('should prepare correct upload path for file', () => { it('should prepare correct upload path for file', () => {
fileSelectDirective.uploader.queue.push(new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{})); fileSelectDirective.uploader.queue.push(new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{}));
component.projectName = "newProject.gns3"; component.projectNameForm.controls['projectName'].setValue("newProject");
component.prepareUploadPath(); component.onImportClick();
expect(fileSelectDirective.uploader.queue[0].url).toContain("localhost:80"); expect(fileSelectDirective.uploader.queue[0].url).toContain("localhost:80");
expect(fileSelectDirective.uploader.queue[0].url).toContain("newProject"); expect(fileSelectDirective.uploader.queue[0].url).toContain("newProject");
@ -136,4 +140,30 @@ describe('ImportProjectDialogComponent', () => {
expect(component.stepper.next).toHaveBeenCalled(); expect(component.stepper.next).toHaveBeenCalled();
}); });
it('should detect if file input is empty', () => {
component.projectNameForm.controls['projectName'].setValue("");
fixture.detectChanges();
spyOn(component.stepper, "next");
spyOn(fileSelectDirective.uploader, 'uploadItem');
component.onImportClick();
expect(component.stepper.next).not.toHaveBeenCalled();
expect(fileSelectDirective.uploader.uploadItem).not.toHaveBeenCalled();
expect(component.projectNameForm.valid).toBeFalsy();
});
it('should sanitize file name input', () => {
component.projectNameForm.controls['projectName'].setValue("[][]");
fixture.detectChanges();
spyOn(component.stepper, "next");
spyOn(fileSelectDirective.uploader, 'uploadItem');
component.onImportClick();
expect(component.stepper.next).not.toHaveBeenCalled();
expect(fileSelectDirective.uploader.uploadItem).not.toHaveBeenCalled();
expect(component.projectNameForm.valid).toBeFalsy();
});
}); });

View File

@ -3,50 +3,79 @@ import { MatStepper, MatDialogRef, MAT_DIALOG_DATA } from "@angular/material";
import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload'; import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload';
import { Server } from '../../../models/server'; import { Server } from '../../../models/server';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
export class Validator {
static projectNameValidator(projectName) {
var pattern = new RegExp(/[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/);
if(!pattern.test(projectName.value)) {
return null;
}
return { invalidName: true }
}
}
@Component({ @Component({
selector: 'app-import-project-dialog', selector: 'app-import-project-dialog',
templateUrl: 'import-project-dialog.component.html', templateUrl: 'import-project-dialog.component.html',
styleUrls: ['import-project-dialog.component.css'], styleUrls: ['import-project-dialog.component.css']
}) })
export class ImportProjectDialogComponent implements OnInit { export class ImportProjectDialogComponent implements OnInit {
uploader: FileUploader; uploader: FileUploader;
server : Server; server : Server;
projectName : string;
isImportEnabled : boolean = false; isImportEnabled : boolean = false;
isFinishEnabled : boolean = false; isFinishEnabled : boolean = false;
errorMessage : string; resultMessage : string = "The project is being imported... Please wait";
projectNameForm: FormGroup;
submitted: boolean = false;
@ViewChild('stepper') stepper: MatStepper; @ViewChild('stepper') stepper: MatStepper;
constructor( constructor(
public dialogRef: MatDialogRef<ImportProjectDialogComponent>, public dialogRef: MatDialogRef<ImportProjectDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any){} @Inject(MAT_DIALOG_DATA) public data: any,
private formBuilder: FormBuilder){
this.projectNameForm = this.formBuilder.group({
projectName: new FormControl(null, [Validators.required, Validator.projectNameValidator])
});
}
ngOnInit(){ ngOnInit(){
this.uploader = new FileUploader({}); this.uploader = new FileUploader({});
this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; }; this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; };
} }
get form() {
return this.projectNameForm.controls;
}
uploadProjectFile(event) : void{ uploadProjectFile(event) : void{
this.projectName = event.target.files[0].name.split(".")[0]; this.projectNameForm.controls['projectName'].setValue(event.target.files[0].name.split(".")[0]);
this.isImportEnabled = true; this.isImportEnabled = true;
} }
onImportClick() : void{ onImportClick() : void{
if(this.validateProjectName()){ if (this.projectNameForm.invalid){
this.prepareUploadPath(); this.submitted = true;
} else {
const url = this.prepareUploadPath();
this.uploader.queue.forEach(elem => elem.url = url);
this.stepper.selected.completed = true; this.stepper.selected.completed = true;
this.stepper.next(); this.stepper.next();
let itemToUpload = this.uploader.queue[0];
const itemToUpload = this.uploader.queue[0];
this.uploader.uploadItem(itemToUpload); this.uploader.uploadItem(itemToUpload);
this.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { this.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {
this.errorMessage = response; this.resultMessage = response;
this.isFinishEnabled = true; this.isFinishEnabled = true;
}; };
this.uploader.onSuccessItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { this.uploader.onSuccessItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {
this.resultMessage = "Project was imported succesfully!";
this.isFinishEnabled = true; this.isFinishEnabled = true;
}; };
} }
@ -64,16 +93,11 @@ export class ImportProjectDialogComponent implements OnInit {
onDeleteClick() : void{ onDeleteClick() : void{
this.uploader.queue.pop(); this.uploader.queue.pop();
this.isImportEnabled = false; this.isImportEnabled = false;
this.projectName = ""; this.projectNameForm.controls['projectName'].setValue("");
} }
prepareUploadPath() : void{ prepareUploadPath() : string{
let url = `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${this.projectName}`; const projectName = this.projectNameForm.controls['projectName'].value;
this.uploader.queue.forEach(elem => elem.url = url); return `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${projectName}`;
}
validateProjectName() : boolean{
var pattern = new RegExp(/[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/);
return !pattern.test(this.projectName);
} }
} }