mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-04-08 02:54:16 +00:00
initial implementation of projects importing
This commit is contained in:
parent
88ae3704dc
commit
4084fb39e0
@ -47,6 +47,7 @@
|
||||
"d3-ng2-service": "^2.1.0",
|
||||
"electron-settings": "^3.2.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"ng2-file-upload": "^1.3.0",
|
||||
"ngx-electron": "^1.0.4",
|
||||
"notosans-fontface": "^1.1.0",
|
||||
"npm-check-updates": "^2.14.1",
|
||||
@ -62,10 +63,10 @@
|
||||
"@angular/cli": "^6.0.8",
|
||||
"@angular/compiler-cli": "^6.0.7",
|
||||
"@angular/language-service": "^6.0.7",
|
||||
"@sentry/electron": "^0.7.0",
|
||||
"@types/jasmine": "~2.8.8",
|
||||
"@types/jasminewd2": "~2.0.2",
|
||||
"@types/node": "~10.5.2",
|
||||
"@sentry/electron": "^0.7.0",
|
||||
"codelyzer": "~4.4.2",
|
||||
"electron": "2.0.4",
|
||||
"electron-builder": "^20.19.2",
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatToolbarModule,
|
||||
MatStepperModule,
|
||||
MatIconModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
@ -32,7 +33,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { HotkeyModule } from 'angular2-hotkeys';
|
||||
import { PersistenceModule } from 'angular-persistence';
|
||||
import { NgxElectronModule } from 'ngx-electron';
|
||||
|
||||
import { FileUploadModule } from 'ng2-file-upload';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
|
||||
import { VersionService } from './services/version.service';
|
||||
@ -48,6 +49,7 @@ import { ApplianceService } from "./services/appliance.service";
|
||||
import { LinkService } from "./services/link.service";
|
||||
|
||||
import { ProjectsComponent } from './components/projects/projects.component';
|
||||
import { ImportProjectDialogComponent } from './components/projects/import-project-dialog/import-project-dialog.component';
|
||||
import { DefaultLayoutComponent } from './layouts/default-layout/default-layout.component';
|
||||
import { ProgressDialogComponent } from './common/progress-dialog/progress-dialog.component';
|
||||
import { AppComponent } from './app.component';
|
||||
@ -106,6 +108,7 @@ if (environment.production) {
|
||||
AddServerDialogComponent,
|
||||
CreateSnapshotDialogComponent,
|
||||
ProjectsComponent,
|
||||
ImportProjectDialogComponent,
|
||||
DefaultLayoutComponent,
|
||||
ProgressDialogComponent,
|
||||
NodeContextMenuComponent,
|
||||
@ -148,10 +151,12 @@ if (environment.production) {
|
||||
MatSortModule,
|
||||
MatSelectModule,
|
||||
MatTooltipModule,
|
||||
MatStepperModule,
|
||||
CartographyModule,
|
||||
HotkeyModule.forRoot(),
|
||||
PersistenceModule,
|
||||
NgxElectronModule
|
||||
NgxElectronModule,
|
||||
FileUploadModule
|
||||
],
|
||||
providers: [
|
||||
SettingsService,
|
||||
@ -184,7 +189,8 @@ if (environment.production) {
|
||||
AddServerDialogComponent,
|
||||
CreateSnapshotDialogComponent,
|
||||
ProgressDialogComponent,
|
||||
ApplianceListDialogComponent
|
||||
ApplianceListDialogComponent,
|
||||
ImportProjectDialogComponent
|
||||
],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
|
@ -0,0 +1,9 @@
|
||||
.file-name-form {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
width:250px;
|
||||
}
|
||||
|
||||
.non-visible {
|
||||
display: none;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<h1 mat-dialog-title>Import project</h1>
|
||||
<mat-horizontal-stepper #stepper [linear]="true">
|
||||
<mat-step label="Choose file" editable="false" completed="false">
|
||||
<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>
|
||||
<mat-form-field class="file-name-form">
|
||||
<input matInput tabindex="1" (input)="validateInput()" [(ngModel)]="projectName" placeholder="Please enter name">
|
||||
</mat-form-field>
|
||||
<button class="material-icons" (click)="onDeleteClick()">delete</button>
|
||||
<div class="buttons-bar">
|
||||
<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>
|
||||
</div>
|
||||
</mat-step>
|
||||
<mat-step label="Progress" editable="false" completed="false">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': uploader.progress + '%' }"></div>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{errorMessage}}</span>
|
||||
</div>
|
||||
<div class="buttons-bar">
|
||||
<button mat-button ng-disabled="!isFinishEnabled" (click)="onNoClick()" tabindex="2" mat-raised-button color="primary">Finish</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
</mat-horizontal-stepper>
|
@ -0,0 +1,139 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ImportProjectDialogComponent } from "./import-project-dialog.component";
|
||||
import { Server } from "../../../models/server";
|
||||
import { MatInputModule, MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule, MatStepperModule, MatFormFieldModule, MatDialogRef, MatDialog, MAT_DIALOG_DATA } from "@angular/material";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { FileUploadModule, FileSelectDirective, FileItem, FileUploader } from "ng2-file-upload";
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('ImportProjectDialogComponent', () => {
|
||||
let component: ImportProjectDialogComponent;
|
||||
let fixture: ComponentFixture<ImportProjectDialogComponent>;
|
||||
let server: Server;
|
||||
let dialog: MatDialog;
|
||||
let debugElement: DebugElement;
|
||||
let fileSelectDirective: FileSelectDirective;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MatTableModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
MatSortModule,
|
||||
MatDialogModule,
|
||||
MatStepperModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
NoopAnimationsModule,
|
||||
FileUploadModule,
|
||||
FormsModule,
|
||||
RouterTestingModule.withRoutes([]),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef },
|
||||
{ provide: MAT_DIALOG_DATA }
|
||||
],
|
||||
declarations : [ImportProjectDialogComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
dialog = TestBed.get(MatDialog);
|
||||
server = new Server();
|
||||
server.ip = "localhost";
|
||||
server.port = 80;
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImportProjectDialogComponent);
|
||||
debugElement = fixture.debugElement;
|
||||
component = fixture.componentInstance;
|
||||
component.server = server;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
debugElement = fixture.debugElement.query(By.directive(FileSelectDirective));
|
||||
fileSelectDirective = debugElement.injector.get(FileSelectDirective) as FileSelectDirective;
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(fixture).toBeDefined();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set file uploader', () => {
|
||||
expect(fileSelectDirective).toBeDefined();
|
||||
expect(fileSelectDirective.uploader).toBe(component.uploader);
|
||||
});
|
||||
|
||||
it('should handle file adding', () => {
|
||||
spyOn(fileSelectDirective.uploader, 'addToQueue');
|
||||
|
||||
fileSelectDirective.onChange();
|
||||
|
||||
const expectedArguments = [ debugElement.nativeElement.files,
|
||||
fileSelectDirective.getOptions(),
|
||||
fileSelectDirective.getFilters() ];
|
||||
expect(fileSelectDirective.uploader.addToQueue).toHaveBeenCalledWith(...expectedArguments);
|
||||
});
|
||||
|
||||
it('should call uploading item', () => {
|
||||
spyOn(fileSelectDirective.uploader, 'uploadItem');
|
||||
|
||||
component.onImportClick();
|
||||
|
||||
expect(fileSelectDirective.uploader.uploadItem).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call uploading item with correct arguments', () => {
|
||||
let fileItem = new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{});
|
||||
fileSelectDirective.uploader.queue.push(fileItem);
|
||||
spyOn(fileSelectDirective.uploader, 'uploadItem');
|
||||
|
||||
component.onImportClick();
|
||||
|
||||
expect(fileSelectDirective.uploader.uploadItem).toHaveBeenCalledWith(fileItem);
|
||||
});
|
||||
|
||||
it('should handle file change event', () => {
|
||||
let input = fixture.debugElement.query(By.css('input[type=file]')).nativeElement;
|
||||
spyOn(component, 'uploadProjectFile');
|
||||
|
||||
input.dispatchEvent(new Event('change'));
|
||||
|
||||
expect(component.uploadProjectFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should clear queue after calling delete', () => {
|
||||
fileSelectDirective.uploader.queue.push(new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{}));
|
||||
spyOn(fileSelectDirective.uploader.queue, "pop");
|
||||
|
||||
component.onDeleteClick();
|
||||
|
||||
expect(fileSelectDirective.uploader.queue.pop).toHaveBeenCalled();
|
||||
expect(fileSelectDirective.uploader.queue[0]).toBeNull;
|
||||
});
|
||||
|
||||
it('should prepare correct upload path for file', () => {
|
||||
fileSelectDirective.uploader.queue.push(new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{}));
|
||||
component.projectName = "newProject.gns3";
|
||||
|
||||
component.prepareUploadPath();
|
||||
|
||||
expect(fileSelectDirective.uploader.queue[0].url).toContain("localhost:80");
|
||||
expect(fileSelectDirective.uploader.queue[0].url).toContain("newProject");
|
||||
});
|
||||
|
||||
it('should navigate to next step after clicking import', () => {
|
||||
let fileItem = new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{});
|
||||
fileSelectDirective.uploader.queue.push(fileItem);
|
||||
spyOn(component.stepper, "next");
|
||||
|
||||
component.onImportClick();
|
||||
|
||||
expect(component.stepper.next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -0,0 +1,79 @@
|
||||
import { Component, OnInit, Inject, ViewChild } from '@angular/core';
|
||||
import { MatStepper, MatDialogRef, MAT_DIALOG_DATA } from "@angular/material";
|
||||
import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload';
|
||||
import { Server } from '../../../models/server';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@Component({
|
||||
selector: 'app-import-project-dialog',
|
||||
templateUrl: 'import-project-dialog.component.html',
|
||||
styleUrls: ['import-project-dialog.component.css'],
|
||||
})
|
||||
export class ImportProjectDialogComponent implements OnInit {
|
||||
uploader: FileUploader;
|
||||
server : Server;
|
||||
projectName : string;
|
||||
isImportEnabled : boolean = false;
|
||||
isFinishEnabled : boolean = false;
|
||||
errorMessage : string;
|
||||
|
||||
@ViewChild('stepper') stepper: MatStepper;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ImportProjectDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any){}
|
||||
|
||||
ngOnInit(){
|
||||
this.uploader = new FileUploader({});
|
||||
this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; };
|
||||
}
|
||||
|
||||
uploadProjectFile(event) : void{
|
||||
this.projectName = event.target.files[0].name.split(".")[0];
|
||||
this.isImportEnabled = true;
|
||||
}
|
||||
|
||||
onImportClick() : void{
|
||||
if(this.validateProjectName()){
|
||||
this.prepareUploadPath();
|
||||
this.stepper.selected.completed = true;
|
||||
this.stepper.next();
|
||||
let itemToUpload = this.uploader.queue[0];
|
||||
this.uploader.uploadItem(itemToUpload);
|
||||
|
||||
this.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {
|
||||
this.errorMessage = response;
|
||||
this.isFinishEnabled = true;
|
||||
};
|
||||
|
||||
this.uploader.onSuccessItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {
|
||||
this.isFinishEnabled = true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onNoClick() : void{
|
||||
this.uploader.cancelAll();
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onFinishClick() : void{
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onDeleteClick() : void{
|
||||
this.uploader.queue.pop();
|
||||
this.isImportEnabled = false;
|
||||
this.projectName = "";
|
||||
}
|
||||
|
||||
prepareUploadPath() : void{
|
||||
let url = `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${this.projectName}`;
|
||||
this.uploader.queue.forEach(elem => elem.url = url);
|
||||
}
|
||||
|
||||
validateProjectName() : boolean{
|
||||
var pattern = new RegExp(/[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/);
|
||||
return !pattern.test(this.projectName);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
.import-button {
|
||||
margin-right:10px
|
||||
}
|
@ -34,5 +34,8 @@
|
||||
</mat-table>
|
||||
</div>
|
||||
|
||||
<div class="buttons-bar">
|
||||
<button mat-raised-button color="primary" (click)="importProject()" class="import-button">Import project</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatIconModule, MatSortModule, MatTableModule, MatTooltipModule } from "@angular/material";
|
||||
import { MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule } from "@angular/material";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
|
||||
@ -34,6 +34,7 @@ describe('ProjectsComponent', () => {
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
MatSortModule,
|
||||
MatDialogModule,
|
||||
NoopAnimationsModule,
|
||||
RouterTestingModule.withRoutes([]),
|
||||
],
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { MatSort, MatSortable } from "@angular/material";
|
||||
import { MatSort, MatSortable, MatDialog } from "@angular/material";
|
||||
|
||||
import { DataSource } from "@angular/cdk/collections";
|
||||
|
||||
@ -14,6 +14,7 @@ import { ServerService } from "../../services/server.service";
|
||||
import { SettingsService, Settings } from "../../services/settings.service";
|
||||
import { ProgressService } from "../../common/progress/progress.service";
|
||||
|
||||
import { ImportProjectDialogComponent } from './import-project-dialog/import-project-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-projects',
|
||||
@ -33,7 +34,8 @@ export class ProjectsComponent implements OnInit {
|
||||
private serverService: ServerService,
|
||||
private projectService: ProjectService,
|
||||
private settingsService: SettingsService,
|
||||
private progressService: ProgressService
|
||||
private progressService: ProgressService,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
|
||||
}
|
||||
@ -96,6 +98,18 @@ export class ProjectsComponent implements OnInit {
|
||||
this.progressService.deactivate();
|
||||
});
|
||||
}
|
||||
|
||||
importProject(){
|
||||
const dialogRef = this.dialog.open(ImportProjectDialogComponent, {
|
||||
width: '550px',
|
||||
});
|
||||
let instance = dialogRef.componentInstance;
|
||||
instance.server = this.server;
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user