diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3a399d0e..c99e98e2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -274,8 +274,12 @@ import { NodeConsoleService } from './services/nodeConsole.service'; import { HttpConsoleNewTabActionComponent } from './components/project-map/context-menu/actions/http-console-new-tab/http-console-new-tab-action.component'; import { WebConsoleFullWindowComponent } from './components/web-console-full-window/web-console-full-window.component'; import { ConsoleGuard } from './guards/console-guard'; +import { NewTemplateDialogComponent } from './components/project-map/new-template-dialog/new-template-dialog.component'; +import { ApplianceService } from './services/appliances.service'; +import { DataSourceFilter } from './filters/dataSourceFilter'; import { ChangeHostnameActionComponent } from './components/project-map/context-menu/actions/change-hostname/change-hostname-action.component'; import { ChangeHostnameDialogComponent } from './components/project-map/change-hostname-dialog/change-hostname-dialog.component'; +import { ApplianceInfoDialogComponent } from './components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component'; if (environment.production) { Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', { @@ -389,6 +393,7 @@ if (environment.production) { SearchFilter, DateFilter, NameFilter, + DataSourceFilter, TemplateFilter, ProjectsFilter, ListOfSnapshotsComponent, @@ -463,8 +468,10 @@ if (environment.production) { ConsoleWrapperComponent, HttpConsoleNewTabActionComponent, WebConsoleFullWindowComponent, + NewTemplateDialogComponent, ChangeHostnameActionComponent, - ChangeHostnameDialogComponent + ChangeHostnameDialogComponent, + ApplianceInfoDialogComponent ], imports: [ BrowserModule, @@ -559,7 +566,8 @@ if (environment.production) { NodeConsoleService, ServerResolve, ConsoleGuard, - Title + Title, + ApplianceService ], entryComponents: [ AddServerDialogComponent, @@ -603,7 +611,9 @@ if (environment.production) { ConfirmationBottomSheetComponent, ConfigDialogComponent, AdbutlerComponent, - ChangeHostnameDialogComponent + NewTemplateDialogComponent, + ChangeHostnameDialogComponent, + ApplianceInfoDialogComponent ], bootstrap: [AppComponent] }) diff --git a/src/app/cartography/components/d3-map/d3-map.component.ts b/src/app/cartography/components/d3-map/d3-map.component.ts index 6f2a1746..cdcaad70 100644 --- a/src/app/cartography/components/d3-map/d3-map.component.ts +++ b/src/app/cartography/components/d3-map/d3-map.component.ts @@ -206,11 +206,11 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy { } updateGrid() { - this.nodeGridX = (this.project.scene_width/2 - (Math.floor(this.project.scene_width/2 / this.project.grid_size) * this.project.grid_size)); - this.nodeGridY = (this.project.scene_height/2 - (Math.floor(this.project.scene_height/2 / this.project.grid_size) * this.project.grid_size)); + if (this.project.grid_size && this.project.grid_size > 0) this.nodeGridX = (this.project.scene_width/2 - (Math.floor(this.project.scene_width/2 / this.project.grid_size) * this.project.grid_size)); + if (this.project.grid_size && this.project.grid_size > 0) this.nodeGridY = (this.project.scene_height/2 - (Math.floor(this.project.scene_height/2 / this.project.grid_size) * this.project.grid_size)); - this.drawingGridX = (this.project.scene_width/2 - (Math.floor(this.project.scene_width/2 / this.project.drawing_grid_size) * this.project.drawing_grid_size)); - this.drawingGridY = (this.project.scene_height/2 - (Math.floor(this.project.scene_height/2 / this.project.drawing_grid_size) * this.project.drawing_grid_size)); + if (this.project.drawing_grid_size && this.project.drawing_grid_size > 0) this.drawingGridX = (this.project.scene_width/2 - (Math.floor(this.project.scene_width/2 / this.project.drawing_grid_size) * this.project.drawing_grid_size)); + if (this.project.drawing_grid_size && this.project.drawing_grid_size > 0) this.drawingGridY = (this.project.scene_height/2 - (Math.floor(this.project.scene_height/2 / this.project.drawing_grid_size) * this.project.drawing_grid_size)); } @HostListener('window:resize', ['$event']) diff --git a/src/app/components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component.html b/src/app/components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component.html new file mode 100644 index 00000000..756720fa --- /dev/null +++ b/src/app/components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component.html @@ -0,0 +1,23 @@ +
+

{{appliance.name}}

+
+
+
+ Vendor: {{appliance.vendor_name}} +
+
+ Status: {{appliance.status}} +
+
+ Maintainer: {{appliance.maintainer}} +
+
+ Adapters: {{appliance.qemu.adapters}} +
+
+ Console type: {{appliance.qemu.console_type}} +
+
+
+ +
diff --git a/src/app/components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component.scss b/src/app/components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component.ts b/src/app/components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component.ts new file mode 100644 index 00000000..069ade1b --- /dev/null +++ b/src/app/components/project-map/new-template-dialog/appliance-info-dialog/appliance-info-dialog.component.ts @@ -0,0 +1,20 @@ +import { Component, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; +import { Appliance } from '../../../../models/appliance'; + +@Component({ + selector: 'appliance-info-dialog', + templateUrl: 'appliance-info-dialog.component.html', +}) +export class ApplianceInfoDialogComponent { + public appliance: Appliance; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any + ) {} + + onNoClick(): void { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/new-template-dialog/new-template-dialog.component.html b/src/app/components/project-map/new-template-dialog/new-template-dialog.component.html new file mode 100644 index 00000000..d68d1e26 --- /dev/null +++ b/src/app/components/project-map/new-template-dialog/new-template-dialog.component.html @@ -0,0 +1,169 @@ +

Add new template

+ + + + Please select how you want to create new template + + + Install new appliance from the GNS server
+ Import an appliance file +
+ +
+ + +
+
+ + + {{actionTitle}} + + +
+ + + + + + + {{category}} + + +
+ + + + Name + {{row.name}} + + + + Emulator + {{row.emulator}} + + + + Vendor + {{row.vendor_name}} + + + + Actions + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + +
+
+ + + {{secondActionTitle}} + + +
+ Server type
+ + Install the appliance locally + Install the appliance on the GNS3 VM + +
+
+ Qemu binary
+ + + {{binary.path}} + + +
+
+ Install required files + + +
+
+ {{image.filename}} +
+
+ + + +
+
+
+
+
+ +
+ +
+ + +
+
+
diff --git a/src/app/components/project-map/new-template-dialog/new-template-dialog.component.scss b/src/app/components/project-map/new-template-dialog/new-template-dialog.component.scss new file mode 100644 index 00000000..1eb66552 --- /dev/null +++ b/src/app/components/project-map/new-template-dialog/new-template-dialog.component.scss @@ -0,0 +1,64 @@ +.radio-button { + margin-bottom: 30px; +} + +.tableHeader { + width: 100%; +} + +.filter-field { + width: 100%; +} + +.example-container { + display: flex; + flex-direction: column; + max-height: 500px; + min-width: 300px; +} + +.mat-table { + overflow: auto; + max-height: 500px; +} + +.element-row { + position: relative; +} + +.element-row:not(.expanded) { + cursor: pointer; +} + +.element-row:not(.expanded):hover { + background: #f5f5f5; +} + +.element-row.expanded { + border-bottom-color: transparent; +} + +.list-item { + display: flex; + justify-content: space-between; + width: 100%; +} + +.button { + margin-left: 10px; +} + +.create-button { + width: 100%; + margin-top: 10px; + margin-bottom: 10px; +} + +.radio-button { + width: 50%; + padding-top: 20px; +} + +.selection-group { + padding-bottom: 20px; +} diff --git a/src/app/components/project-map/new-template-dialog/new-template-dialog.component.ts b/src/app/components/project-map/new-template-dialog/new-template-dialog.component.ts new file mode 100644 index 00000000..e90c4097 --- /dev/null +++ b/src/app/components/project-map/new-template-dialog/new-template-dialog.component.ts @@ -0,0 +1,256 @@ +import { Component, Input, OnInit, ChangeDetectorRef, ViewChild } from '@angular/core'; +import { MatDialogRef, Sort, MatTableDataSource, MatPaginator, MatDialog, MatStepper, MatSelectionList, MatSelectionListChange } from '@angular/material'; +import { Server } from '../../../models/server'; +import { Node } from '../../../cartography/models/node'; +import { Project } from '../../../models/project'; +import { ApplianceService } from '../../../services/appliances.service'; +import { Appliance, Image } from '../../../models/appliance'; +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { FileUploader, FileItem, ParsedResponseHeaders } from 'ng2-file-upload'; +import { ToasterService } from '../../../services/toaster.service'; +import { ApplianceInfoDialogComponent } from './appliance-info-dialog/appliance-info-dialog.component'; +import { QemuBinary } from '../../../models/qemu/qemu-binary'; +import { QemuService } from '../../../services/qemu.service'; + +@Component({ + selector: 'app-new-template-dialog', + templateUrl: './new-template-dialog.component.html', + styleUrls: ['./new-template-dialog.component.scss'], + animations: [ + trigger('detailExpand', [ + state('collapsed', style({ height: '0px', minHeight: '0', visibility: 'hidden' })), + state('expanded', style({ height: '*', visibility: 'visible' })), + transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), + ]), + ], +}) +export class NewTemplateDialogComponent implements OnInit { + @Input() server: Server; + @Input() project: Project; + + uploader: FileUploader; + uploaderImage: FileUploader; + + public action: string = 'install'; + public actionTitle: string = 'Install appliance from server'; + public secondActionTitle: string = 'Appliance settings'; + + public searchText: string = ''; + public allAppliances: Appliance[] = []; + public appliances: Appliance[] = []; + public applianceToInstall: Appliance; + public selectedImages: any[]; + + private isGns3VmChosen = true; + private isLocalComputerChosen = false; + + public qemuBinaries: QemuBinary[] = []; + public selectedBinary: QemuBinary; + + public categories: string[] = ['all categories', 'router', 'multilayer_switch', 'guest', 'firewall']; + public category: string = 'all categories'; + public displayedColumns: string[] = ['name', 'emulator', 'vendor', 'actions']; + + public dataSource: MatTableDataSource; + + @ViewChild(MatPaginator, {static: true}) paginator: MatPaginator; + @ViewChild('stepper', {static: true}) stepper: MatStepper; + + constructor( + public dialogRef: MatDialogRef, + private applianceService: ApplianceService, + private changeDetector: ChangeDetectorRef, + private toasterService: ToasterService, + private qemuService: QemuService, + public dialog: MatDialog + ) {} + + ngOnInit() { + this.applianceService.getAppliances(this.server).subscribe((appliances) => { + this.appliances = appliances; + this.appliances.forEach(appliance => { + if (appliance.docker) appliance.emulator = 'Docker'; + if (appliance.dynamips) appliance.emulator = 'Dynamips'; + if (appliance.iou) appliance.emulator = 'Iou'; + if (appliance.qemu) appliance.emulator = 'Qemu'; + }); + this.allAppliances = appliances; + this.dataSource = new MatTableDataSource(this.allAppliances); + this.dataSource.paginator = this.paginator; + }); + + this.qemuService.getBinaries(this.server).subscribe((binaries) => { + this.qemuBinaries = binaries; + }); + + this.uploader = new FileUploader({}); + this.uploader.onAfterAddingFile = file => { + file.withCredentials = false; + }; + + this.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { + this.toasterService.error('An error has occured'); + }; + + this.uploader.onSuccessItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { + this.toasterService.success('Appliance imported succesfully'); + this.getAppliance(item.url); + }; + + this.uploaderImage = new FileUploader({}); + this.uploaderImage.onAfterAddingFile = file => { + file.withCredentials = false; + }; + + this.uploaderImage.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { + this.toasterService.error('An error has occured'); + }; + + this.uploaderImage.onSuccessItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => { + this.toasterService.success('Image imported succesfully'); + }; + } + + getAppliance(url: string) { + let str = url.split('/v2'); + let appliancePath = str[str.length-1]; + this.applianceService.getAppliance(this.server, appliancePath).subscribe((appliance: Appliance) => { + this.applianceToInstall = appliance; + setTimeout(() => { + this.stepper.next(); + }, 100); + }); + } + + addAppliance(event): void { + let name = event.target.files[0].name.split('-')[0]; + let fileName = event.target.files[0].name; + let file = event.target.files[0]; + let fileReader: FileReader = new FileReader(); + let emulator; + + fileReader.onloadend = () => { + let appliance = JSON.parse(fileReader.result as string); + + if (appliance.docker) emulator = 'docker'; + if (appliance.dynamips) emulator = 'dynamips'; + if (appliance.iou) emulator = 'iou'; + if (appliance.qemu) emulator = 'qemu'; + + const url = this.applianceService.getUploadPath(this.server, emulator, fileName); + this.uploader.queue.forEach(elem => (elem.url = url)); + + const itemToUpload = this.uploader.queue[0]; + (itemToUpload as any).options.disableMultipart = true; + + this.uploader.uploadItem(itemToUpload); + }; + + fileReader.readAsText(file); + } + + filterAppliances(event) { + let temporaryAppliances = this.allAppliances.filter(item => { + return item.name.toLowerCase().includes(this.searchText.toLowerCase()); + }); + + if (this.category === 'all categories' || !this.category) { + this.appliances = temporaryAppliances; + } else { + this.appliances = temporaryAppliances.filter(t => t.category === this.category); + } + + this.dataSource = new MatTableDataSource(this.appliances); + this.dataSource.paginator = this.paginator; + } + + setAction(action: string) { + this.action = action; + if (action === 'install') { + this.actionTitle = 'Install appliance from server'; + } else if (action === 'import') { + this.actionTitle = 'Import an appliance file'; + } + } + + setServerType(serverType: string) { + if (serverType === 'gns3 vm') { + this.isGns3VmChosen = true; + this.isLocalComputerChosen = false; + } else { + this.isGns3VmChosen = false; + this.isLocalComputerChosen = true; + } + } + + sortData(sort: Sort) { + if (!sort.active || sort.direction === '') return; + + let appliances = this.appliances.slice(); + this.appliances = appliances.sort((a, b) => { + const isAsc = sort.direction === 'asc'; + if (sort.active === 'name') { + return compareNames(a.name, b.name, isAsc); + } else if (sort.active === 'emulator') { + return compareNames(a.emulator, b.emulator, isAsc); + } else if (sort.active === 'vendor') { + return compareNames(a.vendor_name, b.vendor_name, isAsc); + } else return 0; + }); + } + + onCloseClick() { + this.dialogRef.close(); + } + + install(object: Appliance) { + this.applianceToInstall = object; + setTimeout(() => { + this.stepper.next(); + }, 100); + } + + showInfo(object: Appliance) { + let dialogRef = this.dialog.open(ApplianceInfoDialogComponent, { + width: '250px', + data: {appliance: object} + }); + dialogRef.componentInstance.appliance = object; + } + + importImage(event) { + let name = event.target.files[0].name.split('-')[0]; + let fileName = event.target.files[0].name; + let file = event.target.files[0]; + let fileReader: FileReader = new FileReader(); + let emulator; + + fileReader.onloadend = () => { + if (this.applianceToInstall.qemu) emulator = 'qemu'; + + const url = this.applianceService.getUploadPath(this.server, emulator, fileName); + this.uploaderImage.queue.forEach(elem => (elem.url = url)); + + const itemToUpload = this.uploaderImage.queue[0]; + (itemToUpload as any).options.disableMultipart = true; + + this.uploaderImage.uploadItem(itemToUpload); + }; + + fileReader.readAsText(file); + } + + downloadImage(image: Image) { + window.open(image.download_url); + } + + create() { + + } +} + +function compareNames(a: string, b: string, isAsc: boolean) { + a = a.toLowerCase(); + b = b.toLowerCase(); + return (a < b ? -1 : 1) * (isAsc ? 1 : -1); +} diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index 236e10a4..8a215389 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -59,6 +59,10 @@ info Go to system status +