mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-06-14 04:58:07 +00:00
Merge pull request #171 from GNS3/importing-projects
Projects importing
This commit is contained in:
@ -47,6 +47,7 @@
|
|||||||
"d3-ng2-service": "^2.1.0",
|
"d3-ng2-service": "^2.1.0",
|
||||||
"electron-settings": "^3.2.0",
|
"electron-settings": "^3.2.0",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
|
"ng2-file-upload": "^1.3.0",
|
||||||
"ngx-electron": "^1.0.4",
|
"ngx-electron": "^1.0.4",
|
||||||
"notosans-fontface": "^1.1.0",
|
"notosans-fontface": "^1.1.0",
|
||||||
"npm-check-updates": "^2.14.1",
|
"npm-check-updates": "^2.14.1",
|
||||||
@ -62,10 +63,10 @@
|
|||||||
"@angular/cli": "^6.0.8",
|
"@angular/cli": "^6.0.8",
|
||||||
"@angular/compiler-cli": "^6.0.7",
|
"@angular/compiler-cli": "^6.0.7",
|
||||||
"@angular/language-service": "^6.0.7",
|
"@angular/language-service": "^6.0.7",
|
||||||
|
"@sentry/electron": "^0.7.0",
|
||||||
"@types/jasmine": "~2.8.8",
|
"@types/jasmine": "~2.8.8",
|
||||||
"@types/jasminewd2": "~2.0.2",
|
"@types/jasminewd2": "~2.0.2",
|
||||||
"@types/node": "~10.5.2",
|
"@types/node": "~10.5.2",
|
||||||
"@sentry/electron": "^0.7.0",
|
|
||||||
"codelyzer": "~4.4.2",
|
"codelyzer": "~4.4.2",
|
||||||
"electron": "2.0.4",
|
"electron": "2.0.4",
|
||||||
"electron-builder": "^20.19.2",
|
"electron-builder": "^20.19.2",
|
||||||
|
@ -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';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { HotkeyModule } from 'angular2-hotkeys';
|
import { HotkeyModule } from 'angular2-hotkeys';
|
||||||
import { PersistenceModule } from 'angular-persistence';
|
import { PersistenceModule } from 'angular-persistence';
|
||||||
import { NgxElectronModule } from 'ngx-electron';
|
import { NgxElectronModule } from 'ngx-electron';
|
||||||
|
import { FileUploadModule } from 'ng2-file-upload';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
|
||||||
import { VersionService } from './services/version.service';
|
import { VersionService } from './services/version.service';
|
||||||
@ -28,6 +28,8 @@ import { ApplianceService } from "./services/appliance.service";
|
|||||||
import { LinkService } from "./services/link.service";
|
import { LinkService } from "./services/link.service";
|
||||||
|
|
||||||
import { ProjectsComponent } from './components/projects/projects.component';
|
import { ProjectsComponent } from './components/projects/projects.component';
|
||||||
|
import { ImportProjectDialogComponent } from './components/projects/import-project-dialog/import-project-dialog.component';
|
||||||
|
import { ImportProjectConfirmationDialogComponent} from './components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component';
|
||||||
import { DefaultLayoutComponent } from './layouts/default-layout/default-layout.component';
|
import { DefaultLayoutComponent } from './layouts/default-layout/default-layout.component';
|
||||||
import { ProgressDialogComponent } from './common/progress-dialog/progress-dialog.component';
|
import { ProgressDialogComponent } from './common/progress-dialog/progress-dialog.component';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
@ -92,6 +94,8 @@ if (environment.production) {
|
|||||||
SnapshotMenuItemComponent,
|
SnapshotMenuItemComponent,
|
||||||
SnapshotsComponent,
|
SnapshotsComponent,
|
||||||
ProjectsComponent,
|
ProjectsComponent,
|
||||||
|
ImportProjectDialogComponent,
|
||||||
|
ImportProjectConfirmationDialogComponent,
|
||||||
DefaultLayoutComponent,
|
DefaultLayoutComponent,
|
||||||
ProgressDialogComponent,
|
ProgressDialogComponent,
|
||||||
NodeContextMenuComponent,
|
NodeContextMenuComponent,
|
||||||
@ -113,13 +117,15 @@ if (environment.production) {
|
|||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
CdkTableModule,
|
CdkTableModule,
|
||||||
CartographyModule,
|
CartographyModule,
|
||||||
HotkeyModule.forRoot(),
|
HotkeyModule.forRoot(),
|
||||||
PersistenceModule,
|
PersistenceModule,
|
||||||
NgxElectronModule,
|
NgxElectronModule,
|
||||||
...MATERIAL_IMPORTS
|
FileUploadModule,
|
||||||
|
MATERIAL_IMPORTS
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SettingsService,
|
SettingsService,
|
||||||
@ -153,7 +159,9 @@ if (environment.production) {
|
|||||||
AddServerDialogComponent,
|
AddServerDialogComponent,
|
||||||
CreateSnapshotDialogComponent,
|
CreateSnapshotDialogComponent,
|
||||||
ProgressDialogComponent,
|
ProgressDialogComponent,
|
||||||
ApplianceListDialogComponent
|
ApplianceListDialogComponent,
|
||||||
|
ImportProjectDialogComponent,
|
||||||
|
ImportProjectConfirmationDialogComponent
|
||||||
],
|
],
|
||||||
bootstrap: [ AppComponent ]
|
bootstrap: [ AppComponent ]
|
||||||
})
|
})
|
||||||
|
@ -34,4 +34,3 @@ export class ApplianceComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<span>{{confirmationMessage}}</span>
|
||||||
|
<div mat-dialog-actions *ngIf="!isOpen">
|
||||||
|
<button mat-button class="cancelButton" (click)="onNoClick()" color="accent">No</button>
|
||||||
|
<button mat-button class="confirmButton" (click)="onYesClick()" tabindex="2" mat-raised-button color="primary">Yes</button>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions *ngIf="isOpen">
|
||||||
|
<button mat-button (click)="onNoClick()" color="accent">Ok</button>
|
||||||
|
</div>
|
@ -0,0 +1,131 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MatDialogModule, MatDialog } from "@angular/material";
|
||||||
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
import { Component, NgModule } from '@angular/core';
|
||||||
|
import { Project } from '../../../../models/project';
|
||||||
|
import { ImportProjectConfirmationDialogComponent } from './import-project-confirmation-dialog.component';
|
||||||
|
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||||
|
|
||||||
|
describe('ImportProjectConfirmationDialogComponent', () => {
|
||||||
|
let dialog: MatDialog;
|
||||||
|
let overlayContainerElement: HTMLElement;
|
||||||
|
|
||||||
|
let noop: ComponentFixture<NoopComponent>;
|
||||||
|
let existingProject: Project = {
|
||||||
|
auto_close: false,
|
||||||
|
auto_open: false,
|
||||||
|
auto_start: false,
|
||||||
|
filename: "blank",
|
||||||
|
name: "blank",
|
||||||
|
path: "",
|
||||||
|
project_id: "",
|
||||||
|
scene_height: 100,
|
||||||
|
scene_width: 100,
|
||||||
|
status: "",
|
||||||
|
readonly: false,
|
||||||
|
show_interface_labels: false,
|
||||||
|
show_layers: false,
|
||||||
|
show_grid: false,
|
||||||
|
snap_to_grid: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ DialogTestModule ],
|
||||||
|
providers: [
|
||||||
|
{ provide: OverlayContainer, useFactory: () => {
|
||||||
|
overlayContainerElement = document.createElement('div');
|
||||||
|
return { getContainerElement: () => overlayContainerElement };
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog = TestBed.get(MatDialog);
|
||||||
|
|
||||||
|
noop = TestBed.createComponent(NoopComponent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show correct message if project is open', () => {
|
||||||
|
existingProject.status = "opened";
|
||||||
|
const config = {
|
||||||
|
data: {
|
||||||
|
'existingProject' : existingProject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.open(ImportProjectConfirmationDialogComponent, config);
|
||||||
|
noop.detectChanges();
|
||||||
|
|
||||||
|
const message = overlayContainerElement.querySelector('span');
|
||||||
|
expect(message.textContent).toBe("Project blank is open. You can not overwrite it.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show correct message if project is closed', () => {
|
||||||
|
existingProject.status = "closed";
|
||||||
|
const config = {
|
||||||
|
data: {
|
||||||
|
'existingProject' : existingProject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.open(ImportProjectConfirmationDialogComponent, config);
|
||||||
|
noop.detectChanges();
|
||||||
|
|
||||||
|
const message = overlayContainerElement.querySelector('span');
|
||||||
|
expect(message.textContent).toBe("Project blank already exist, overwrite it?");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false after closing when project is open', () => {
|
||||||
|
existingProject.status = "opened";
|
||||||
|
const config = {
|
||||||
|
data: {
|
||||||
|
'existingProject' : existingProject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let dialogRef = dialog.open(ImportProjectConfirmationDialogComponent, config);
|
||||||
|
noop.detectChanges();
|
||||||
|
const button = overlayContainerElement.querySelector('button');
|
||||||
|
spyOn(dialogRef.componentInstance.dialogRef, 'close');
|
||||||
|
button.click();
|
||||||
|
|
||||||
|
expect(dialogRef.componentInstance.dialogRef.close).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true after choosing overriding', () => {
|
||||||
|
existingProject.status = "closed";
|
||||||
|
const config = {
|
||||||
|
data: {
|
||||||
|
'existingProject' : existingProject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let dialogRef = dialog.open(ImportProjectConfirmationDialogComponent, config);
|
||||||
|
noop.detectChanges();
|
||||||
|
const button: HTMLButtonElement = overlayContainerElement.querySelector('.confirmButton');
|
||||||
|
spyOn(dialogRef.componentInstance.dialogRef, 'close');
|
||||||
|
button.click();
|
||||||
|
|
||||||
|
expect(dialogRef.componentInstance.dialogRef.close).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: ''
|
||||||
|
})
|
||||||
|
class NoopComponent {}
|
||||||
|
|
||||||
|
const TEST_DIRECTIVES = [
|
||||||
|
ImportProjectConfirmationDialogComponent,
|
||||||
|
NoopComponent
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [MatDialogModule, NoopAnimationsModule],
|
||||||
|
exports: TEST_DIRECTIVES,
|
||||||
|
declarations: TEST_DIRECTIVES,
|
||||||
|
entryComponents: [
|
||||||
|
ImportProjectConfirmationDialogComponent
|
||||||
|
],
|
||||||
|
})
|
||||||
|
class DialogTestModule { }
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material";
|
||||||
|
import { Project } from '../../../../models/project';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-import-project-dialog',
|
||||||
|
templateUrl: 'import-project-confirmation-dialog.component.html',
|
||||||
|
styleUrls: ['import-project-confirmation-dialog.component.css']
|
||||||
|
})
|
||||||
|
export class ImportProjectConfirmationDialogComponent implements OnInit {
|
||||||
|
private existingProject : Project;
|
||||||
|
public confirmationMessage : string;
|
||||||
|
public isOpen : boolean;
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<ImportProjectConfirmationDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: any
|
||||||
|
){
|
||||||
|
this.existingProject = data['existingProject']
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(){
|
||||||
|
if(this.existingProject.status === "opened"){
|
||||||
|
this.confirmationMessage = `Project ${this.existingProject.name} is open. You can not overwrite it.`
|
||||||
|
this.isOpen = true;
|
||||||
|
} else {
|
||||||
|
this.confirmationMessage = `Project ${this.existingProject.name} already exist, overwrite it?`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNoClick() : void {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onYesClick() : void {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
.non-visible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-button{
|
||||||
|
height: 50px;
|
||||||
|
width: 20%;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-form {
|
||||||
|
float: right;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-form-field {
|
||||||
|
margin-left: 5%;
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: 0;
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-icon {
|
||||||
|
vertical-align: "middle";
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message-box {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background-color : #0097a7;
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<h1 mat-dialog-title>Import project</h1>
|
||||||
|
<div [hidden]="isFirstStepCompleted">
|
||||||
|
<form [formGroup]="projectNameForm" class="file-name-form">
|
||||||
|
<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()" class="file-button">Choose file</button>
|
||||||
|
<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>
|
||||||
|
<button class="delete-button" [hidden]="!isDeleteVisible">
|
||||||
|
<mat-icon color="primary" (click)="onDeleteClick()" class="delete-icon">clear</mat-icon>
|
||||||
|
</button>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="onNoClick()" color="accent">Cancel</button>
|
||||||
|
<button mat-button [disabled]="!isImportEnabled" (click)="onImportClick()" tabindex="2" mat-raised-button color="primary">Import</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div [hidden]="!isFirstStepCompleted">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': uploader.progress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="result-message-box">
|
||||||
|
<span>{{resultMessage}}</span>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button [disabled]="!isFinishEnabled" (click)="onNoClick()" tabindex="2" mat-raised-button color="primary">Finish</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,229 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ImportProjectDialogComponent, Validator } 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, ParsedResponseHeaders } from "ng2-file-upload";
|
||||||
|
import { FormsModule, ReactiveFormsModule, FormBuilder, FormControl, Validators } from '@angular/forms';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ProjectService } from '../../../services/project.service';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
import { Project } from '../../../models/project';
|
||||||
|
|
||||||
|
export class MockedProjectService {
|
||||||
|
public projects: Project[] = [{
|
||||||
|
auto_close: false,
|
||||||
|
auto_open: false,
|
||||||
|
auto_start: false,
|
||||||
|
filename: "blank",
|
||||||
|
name: "blank",
|
||||||
|
path: "",
|
||||||
|
project_id: "",
|
||||||
|
scene_height: 100,
|
||||||
|
scene_width: 100,
|
||||||
|
status: "opened",
|
||||||
|
readonly: false,
|
||||||
|
show_interface_labels: false,
|
||||||
|
show_layers: false,
|
||||||
|
show_grid: false,
|
||||||
|
snap_to_grid: false,
|
||||||
|
}];
|
||||||
|
|
||||||
|
list(server: Server) {
|
||||||
|
return of(this.projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ImportProjectDialogComponent', () => {
|
||||||
|
let component: ImportProjectDialogComponent;
|
||||||
|
let fixture: ComponentFixture<ImportProjectDialogComponent>;
|
||||||
|
let server: Server;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
let fileSelectDirective: FileSelectDirective;
|
||||||
|
let formBuilder: FormBuilder;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
MatTableModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatSortModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatStepperModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
NoopAnimationsModule,
|
||||||
|
FileUploadModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: MatDialogRef },
|
||||||
|
{ provide: MAT_DIALOG_DATA },
|
||||||
|
{ provide: ProjectService, useClass: MockedProjectService}
|
||||||
|
],
|
||||||
|
declarations : [ImportProjectDialogComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
server = new Server();
|
||||||
|
server.ip = "localhost";
|
||||||
|
server.port = 80;
|
||||||
|
formBuilder = new FormBuilder();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ImportProjectDialogComponent);
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.server = server;
|
||||||
|
component.projectNameForm = formBuilder.group({
|
||||||
|
projectName: new FormControl(null, [Validators.required, Validator.projectNameValidator])
|
||||||
|
});
|
||||||
|
component.projectNameForm.controls['projectName'].setValue("ValidName");
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
debugElement = fixture.debugElement.query(By.directive(FileSelectDirective));
|
||||||
|
fileSelectDirective = debugElement.injector.get(FileSelectDirective) as FileSelectDirective;
|
||||||
|
component.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {};
|
||||||
|
});
|
||||||
|
|
||||||
|
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.projectNameForm.controls['projectName'].setValue("newProject");
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(fileSelectDirective.uploader.queue[0].url).toContain("localhost:80");
|
||||||
|
expect(fileSelectDirective.uploader.queue[0].url).toContain("newProject");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to progress view after clicking import', () => {
|
||||||
|
let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{});
|
||||||
|
fileSelectDirective.uploader.queue.push(fileItem);
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(component.isFirstStepCompleted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect if file input is empty', () => {
|
||||||
|
component.projectNameForm.controls['projectName'].setValue("");
|
||||||
|
fixture.detectChanges();
|
||||||
|
spyOn(fileSelectDirective.uploader, 'uploadItem');
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
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(fileSelectDirective.uploader, 'uploadItem');
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(fileSelectDirective.uploader.uploadItem).not.toHaveBeenCalled();
|
||||||
|
expect(component.projectNameForm.valid).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open confirmation dialog if project with the same exists', () => {
|
||||||
|
component.projectNameForm.controls['projectName'].setValue("blank");
|
||||||
|
spyOn(component, "openConfirmationDialog");
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(component.openConfirmationDialog).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show delete button after selecting project', () => {
|
||||||
|
let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{});
|
||||||
|
fileSelectDirective.uploader.queue.push(fileItem);
|
||||||
|
let event = {
|
||||||
|
target: {
|
||||||
|
files: [ {name : "uploadedFile"} ]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
component.uploadProjectFile(event);
|
||||||
|
|
||||||
|
expect(component.isDeleteVisible).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide delete button after deselecting project', () => {
|
||||||
|
let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{});
|
||||||
|
fileSelectDirective.uploader.queue.push(fileItem);
|
||||||
|
let event = {
|
||||||
|
target: {
|
||||||
|
files: [ {name : "uploadedFile"} ]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
component.uploadProjectFile(event);
|
||||||
|
component.onDeleteClick();
|
||||||
|
|
||||||
|
expect(component.isDeleteVisible).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,146 @@
|
|||||||
|
import { Component, OnInit, Inject, ViewChild } from '@angular/core';
|
||||||
|
import { MatStepper, MatDialogRef, MAT_DIALOG_DATA, MatDialog } from "@angular/material";
|
||||||
|
import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload';
|
||||||
|
import { Server } from '../../../models/server';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||||
|
import { ProjectService } from '../../../services/project.service';
|
||||||
|
import { Project } from '../../../models/project';
|
||||||
|
import { ImportProjectConfirmationDialogComponent } from './import-project-confirmation-dialog/import-project-confirmation-dialog.component';
|
||||||
|
import { ServerResponse } from '../../../models/serverResponse';
|
||||||
|
|
||||||
|
export class Validator {
|
||||||
|
static projectNameValidator(projectName) {
|
||||||
|
var pattern = new RegExp(/[~`!#$%\^&*+=\[\]\\';,/{}|\\":<>\?]/);
|
||||||
|
|
||||||
|
if(!pattern.test(projectName.value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { invalidName: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
isImportEnabled : boolean = false;
|
||||||
|
isFinishEnabled : boolean = false;
|
||||||
|
isDeleteVisible : boolean = false;
|
||||||
|
resultMessage : string = "The project is being imported... Please wait";
|
||||||
|
projectNameForm: FormGroup;
|
||||||
|
submitted: boolean = false;
|
||||||
|
isFirstStepCompleted: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dialog: MatDialog,
|
||||||
|
public dialogRef: MatDialogRef<ImportProjectDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private projectService: ProjectService){
|
||||||
|
this.projectNameForm = this.formBuilder.group({
|
||||||
|
projectName: new FormControl(null, [Validators.required, Validator.projectNameValidator])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(){
|
||||||
|
this.uploader = new FileUploader({});
|
||||||
|
this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; };
|
||||||
|
|
||||||
|
this.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {
|
||||||
|
let serverResponse : ServerResponse = JSON.parse(response);
|
||||||
|
this.resultMessage = "An error occured: " + serverResponse.message;
|
||||||
|
this.isFinishEnabled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.uploader.onCompleteItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {
|
||||||
|
this.resultMessage = "Project was imported succesfully!";
|
||||||
|
this.isFinishEnabled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get form() {
|
||||||
|
return this.projectNameForm.controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProjectFile(event) : void{
|
||||||
|
this.projectNameForm.controls['projectName'].setValue(event.target.files[0].name.split(".")[0]);
|
||||||
|
this.isImportEnabled = true;
|
||||||
|
this.isDeleteVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onImportClick() : void{
|
||||||
|
if (this.projectNameForm.invalid){
|
||||||
|
this.submitted = true;
|
||||||
|
} else {
|
||||||
|
this.projectService
|
||||||
|
.list(this.server)
|
||||||
|
.subscribe((projects: Project[]) => {
|
||||||
|
const projectName = this.projectNameForm.controls['projectName'].value;
|
||||||
|
let existingProject = projects.find(project => project.name === projectName);
|
||||||
|
|
||||||
|
if (existingProject){
|
||||||
|
this.openConfirmationDialog(existingProject);
|
||||||
|
} else {
|
||||||
|
this.importProject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importProject(){
|
||||||
|
const url = this.prepareUploadPath();
|
||||||
|
this.uploader.queue.forEach(elem => elem.url = url);
|
||||||
|
|
||||||
|
this.isFirstStepCompleted = true;
|
||||||
|
|
||||||
|
const itemToUpload = this.uploader.queue[0];
|
||||||
|
this.uploader.uploadItem(itemToUpload);
|
||||||
|
}
|
||||||
|
|
||||||
|
openConfirmationDialog(existingProject: Project) {
|
||||||
|
const dialogRef = this.dialog.open(ImportProjectConfirmationDialogComponent, {
|
||||||
|
width: '300px',
|
||||||
|
height: '150px',
|
||||||
|
data: {
|
||||||
|
'existingProject': existingProject
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((answer: boolean) => {
|
||||||
|
if (answer) {
|
||||||
|
this.projectService.close(this.server, existingProject.project_id).subscribe(() => {
|
||||||
|
this.projectService.delete(this.server, existingProject.project_id).subscribe(() => {
|
||||||
|
this.importProject();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onNoClick() : void{
|
||||||
|
this.uploader.cancelAll();
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFinishClick() : void{
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteClick() : void{
|
||||||
|
this.uploader.queue.pop();
|
||||||
|
this.isImportEnabled = false;
|
||||||
|
this.isDeleteVisible = false;
|
||||||
|
this.projectNameForm.controls['projectName'].setValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareUploadPath() : string{
|
||||||
|
const projectName = this.projectNameForm.controls['projectName'].value;
|
||||||
|
return `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${projectName}`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
.import-button {
|
||||||
|
height: 40px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="default-header">
|
<div class="default-header">
|
||||||
<h1>Projects</h1>
|
<div class="row">
|
||||||
|
<h1 class="col">Projects</h1>
|
||||||
|
<button class="col" mat-raised-button color="primary" (click)="importProject()" class="import-button">Import project</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="default-content">
|
<div class="default-content">
|
||||||
|
|
||||||
@ -33,6 +36,5 @@
|
|||||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
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 { RouterTestingModule } from "@angular/router/testing";
|
||||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ describe('ProjectsComponent', () => {
|
|||||||
MatTooltipModule,
|
MatTooltipModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
|
MatDialogModule,
|
||||||
NoopAnimationsModule,
|
NoopAnimationsModule,
|
||||||
RouterTestingModule.withRoutes([]),
|
RouterTestingModule.withRoutes([]),
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
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";
|
import { DataSource } from "@angular/cdk/collections";
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ import { ServerService } from "../../services/server.service";
|
|||||||
import { SettingsService, Settings } from "../../services/settings.service";
|
import { SettingsService, Settings } from "../../services/settings.service";
|
||||||
import { ProgressService } from "../../common/progress/progress.service";
|
import { ProgressService } from "../../common/progress/progress.service";
|
||||||
|
|
||||||
|
import { ImportProjectDialogComponent } from './import-project-dialog/import-project-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-projects',
|
selector: 'app-projects',
|
||||||
@ -33,7 +34,8 @@ export class ProjectsComponent implements OnInit {
|
|||||||
private serverService: ServerService,
|
private serverService: ServerService,
|
||||||
private projectService: ProjectService,
|
private projectService: ProjectService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
private progressService: ProgressService
|
private progressService: ProgressService,
|
||||||
|
private dialog: MatDialog
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -96,6 +98,18 @@ export class ProjectsComponent implements OnInit {
|
|||||||
this.progressService.deactivate();
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4
src/app/models/serverResponse.ts
Normal file
4
src/app/models/serverResponse.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export class ServerResponse {
|
||||||
|
message: string;
|
||||||
|
status: number;
|
||||||
|
}
|
Reference in New Issue
Block a user