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}}
+
+
+
+
+
+
+ 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
+