mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-06-22 08:30:09 +00:00
Merge pull request #884 from GNS3/Create-templates-from-appliances
Create templates from appliances
This commit is contained in:
@ -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 { 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 { WebConsoleFullWindowComponent } from './components/web-console-full-window/web-console-full-window.component';
|
||||||
import { ConsoleGuard } from './guards/console-guard';
|
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 { 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 { 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) {
|
if (environment.production) {
|
||||||
Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', {
|
Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', {
|
||||||
@ -389,6 +393,7 @@ if (environment.production) {
|
|||||||
SearchFilter,
|
SearchFilter,
|
||||||
DateFilter,
|
DateFilter,
|
||||||
NameFilter,
|
NameFilter,
|
||||||
|
DataSourceFilter,
|
||||||
TemplateFilter,
|
TemplateFilter,
|
||||||
ProjectsFilter,
|
ProjectsFilter,
|
||||||
ListOfSnapshotsComponent,
|
ListOfSnapshotsComponent,
|
||||||
@ -463,8 +468,10 @@ if (environment.production) {
|
|||||||
ConsoleWrapperComponent,
|
ConsoleWrapperComponent,
|
||||||
HttpConsoleNewTabActionComponent,
|
HttpConsoleNewTabActionComponent,
|
||||||
WebConsoleFullWindowComponent,
|
WebConsoleFullWindowComponent,
|
||||||
|
NewTemplateDialogComponent,
|
||||||
ChangeHostnameActionComponent,
|
ChangeHostnameActionComponent,
|
||||||
ChangeHostnameDialogComponent
|
ChangeHostnameDialogComponent,
|
||||||
|
ApplianceInfoDialogComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@ -559,7 +566,8 @@ if (environment.production) {
|
|||||||
NodeConsoleService,
|
NodeConsoleService,
|
||||||
ServerResolve,
|
ServerResolve,
|
||||||
ConsoleGuard,
|
ConsoleGuard,
|
||||||
Title
|
Title,
|
||||||
|
ApplianceService
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
AddServerDialogComponent,
|
AddServerDialogComponent,
|
||||||
@ -603,7 +611,9 @@ if (environment.production) {
|
|||||||
ConfirmationBottomSheetComponent,
|
ConfirmationBottomSheetComponent,
|
||||||
ConfigDialogComponent,
|
ConfigDialogComponent,
|
||||||
AdbutlerComponent,
|
AdbutlerComponent,
|
||||||
ChangeHostnameDialogComponent
|
NewTemplateDialogComponent,
|
||||||
|
ChangeHostnameDialogComponent,
|
||||||
|
ApplianceInfoDialogComponent
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
@ -206,11 +206,11 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateGrid() {
|
updateGrid() {
|
||||||
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.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.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));
|
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));
|
||||||
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.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'])
|
@HostListener('window:resize', ['$event'])
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<div class="title-container">
|
||||||
|
<h1 mat-dialog-title>{{appliance.name}}</h1>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<div>
|
||||||
|
Vendor: {{appliance.vendor_name}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Status: {{appliance.status}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Maintainer: {{appliance.maintainer}}
|
||||||
|
</div>
|
||||||
|
<div *ngIf="appliance.qemu">
|
||||||
|
Adapters: {{appliance.qemu.adapters}}
|
||||||
|
</div>
|
||||||
|
<div *ngIf="appliance.qemu">
|
||||||
|
Console type: {{appliance.qemu.console_type}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="onNoClick()" tabindex="-1" color="accent">Close</button>
|
||||||
|
</div>
|
@ -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<ApplianceInfoDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: any
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onNoClick(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,169 @@
|
|||||||
|
<h1 mat-dialog-title>Add new template</h1>
|
||||||
|
|
||||||
|
<mat-horizontal-stepper [linear]="false" #stepper>
|
||||||
|
<mat-step [stepControl]="firstFormGroup">
|
||||||
|
<ng-template matStepLabel>Please select how you want to create new template</ng-template>
|
||||||
|
|
||||||
|
<mat-radio-group class="radio-group">
|
||||||
|
<mat-radio-button class="radio-button" value="1" (click)="setAction('install')" checked>Install new appliance from the GNS server</mat-radio-button><br/>
|
||||||
|
<mat-radio-button class="radio-button" value="2" (click)="setAction('import')">Import an appliance file</mat-radio-button>
|
||||||
|
</mat-radio-group>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button mat-button matStepperNext>Next</button>
|
||||||
|
<button mat-button (click)="onCloseClick()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
|
<mat-step [stepControl]="secondFormGroup">
|
||||||
|
<ng-template matStepLabel>{{actionTitle}}</ng-template>
|
||||||
|
|
||||||
|
<mat-card [hidden]="!(action==='install')">
|
||||||
|
<div class="tableHeader">
|
||||||
|
<mat-form-field class="filter-field">
|
||||||
|
<input matInput [(ngModel)]="searchText" placeholder="Filter">
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-select (selectionChange)="filterAppliances($event)" [ngModelOptions]="{standalone: true}" placeholder="Category" [(ngModel)]="category">
|
||||||
|
<mat-option *ngFor="let category of categories" [value]="category">
|
||||||
|
{{category}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-table
|
||||||
|
class="mat-table"
|
||||||
|
#table
|
||||||
|
matSort
|
||||||
|
multiTemplateDataRows
|
||||||
|
(matSortChange)= "sortData($event)"
|
||||||
|
[dataSource]="dataSource | datasourcefilter: searchText">
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row"> {{row.name}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="emulator">
|
||||||
|
<mat-header-cell *matHeaderCellDef> Emulator </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row"> {{row.emulator}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="vendor">
|
||||||
|
<mat-header-cell *matHeaderCellDef> Vendor </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row"> {{row.vendor_name}} </mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<mat-header-cell *matHeaderCellDef> Actions </mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row" style="text-align: right">
|
||||||
|
<button mat-icon-button matTooltip="Install" (click)="install(row)">
|
||||||
|
<mat-icon aria-label="Install">archive</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button mat-icon-button matTooltip="Show info" (click)="showInfo(row)">
|
||||||
|
<mat-icon aria-label="Show info">info</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- <ng-container matColumnDef="expandedDetail">
|
||||||
|
<mat-cell *matCellDef="let detail">
|
||||||
|
The symbol for {{detail.element}}
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container> -->
|
||||||
|
|
||||||
|
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||||
|
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||||
|
|
||||||
|
<!-- <mat-row
|
||||||
|
*matRowDef="let row; columns: displayedColumns;"
|
||||||
|
matRipple
|
||||||
|
class="element-row"
|
||||||
|
[class.expanded]="expandedElement == row"
|
||||||
|
(click)="expandedElement = row">
|
||||||
|
</mat-row>
|
||||||
|
<mat-row
|
||||||
|
*matRowDef="let row; columns: ['expandedDetail']; when: isExpansionDetailRow"
|
||||||
|
[@detailExpand]="row.element == expandedElement ? 'expanded' : 'collapsed'"
|
||||||
|
style="overflow: hidden">
|
||||||
|
</mat-row> -->
|
||||||
|
</mat-table>
|
||||||
|
|
||||||
|
<mat-paginator [pageSizeOptions]="[5, 10, 20, 50, 100]" showFirstLastButtons></mat-paginator>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<mat-card [hidden]="action==='install'">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".gns3appliance, .gns3a"
|
||||||
|
class="non-visible"
|
||||||
|
#file
|
||||||
|
(change)="addAppliance($event)"
|
||||||
|
ng2FileSelect
|
||||||
|
[uploader]="uploader"/>
|
||||||
|
<button mat-raised-button color="primary" (click)="file.click()" class="create-button">Click to import appliance</button>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button mat-button matStepperPrevious>Back</button>
|
||||||
|
<button mat-button (click)="onCloseClick()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
|
<mat-step *ngIf="applianceToInstall">
|
||||||
|
<ng-template matStepLabel>{{secondActionTitle}}</ng-template>
|
||||||
|
|
||||||
|
<mat-card [hidden]="!applianceToInstall">
|
||||||
|
<div>
|
||||||
|
Server type<br/>
|
||||||
|
<mat-radio-group class="radio-group">
|
||||||
|
<mat-radio-button [disabled]="true" class="radio-button" value="1" (click)="setServerType('local')">Install the appliance locally</mat-radio-button>
|
||||||
|
<mat-radio-button class="radio-button" value="2" checked (click)="setServerType('gns3 vm')">Install the appliance on the GNS3 VM</mat-radio-button>
|
||||||
|
</mat-radio-group>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Qemu binary<br/>
|
||||||
|
<mat-select
|
||||||
|
class="selection-group"
|
||||||
|
placeholder="Qemu binary"
|
||||||
|
[(ngModel)]="selectedBinary"
|
||||||
|
[ngModelOptions]="{standalone: true}">
|
||||||
|
<mat-option *ngFor="let binary of qemuBinaries" [value]="binary">
|
||||||
|
{{binary.path}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Install required files
|
||||||
|
<mat-list>
|
||||||
|
<mat-list-item *ngFor="let image of applianceToInstall.images">
|
||||||
|
<div class="list-item">
|
||||||
|
<div>
|
||||||
|
{{image.filename}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="non-visible"
|
||||||
|
#file2
|
||||||
|
(change)="importImage($event)"
|
||||||
|
ng2FileSelect
|
||||||
|
[uploader]="uploaderImage"/>
|
||||||
|
<button class="button" mat-raised-button (click)="file2.click()">Import</button>
|
||||||
|
<button class="button" mat-raised-button color="primary" (click)="downloadImage(image)">Download</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
</div>
|
||||||
|
<button class="create-button" mat-raised-button color="primary" (click)="create()">
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button mat-button matStepperPrevious>Back</button>
|
||||||
|
<button mat-button (click)="onCloseClick()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</mat-step>
|
||||||
|
</mat-horizontal-stepper>
|
@ -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;
|
||||||
|
}
|
@ -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<Appliance>;
|
||||||
|
|
||||||
|
@ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
|
||||||
|
@ViewChild('stepper', {static: true}) stepper: MatStepper;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<NewTemplateDialogComponent>,
|
||||||
|
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);
|
||||||
|
}
|
@ -59,6 +59,10 @@
|
|||||||
<mat-icon>info</mat-icon>
|
<mat-icon>info</mat-icon>
|
||||||
<span>Go to system status</span>
|
<span>Go to system status</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-menu-item (click)="addNewTemplate()">
|
||||||
|
<mat-icon>control_point</mat-icon>
|
||||||
|
<span>New template</span>
|
||||||
|
</button>
|
||||||
<app-import-appliance [server]="server" [project]="project"></app-import-appliance>
|
<app-import-appliance [server]="server" [project]="project"></app-import-appliance>
|
||||||
<button mat-menu-item [matMenuTriggerFor]="projectMenu">
|
<button mat-menu-item [matMenuTriggerFor]="projectMenu">
|
||||||
<mat-icon>settings</mat-icon>
|
<mat-icon>settings</mat-icon>
|
||||||
|
@ -67,6 +67,7 @@ import { NodeAddedEvent } from '../template/template-list-dialog/template-list-d
|
|||||||
import { NotificationService } from '../../services/notification.service';
|
import { NotificationService } from '../../services/notification.service';
|
||||||
import { ThemeService } from '../../services/theme.service';
|
import { ThemeService } from '../../services/theme.service';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { NewTemplateDialogComponent } from './new-template-dialog/new-template-dialog.component';
|
||||||
import { NodeConsoleService } from '../../services/nodeConsole.service';
|
import { NodeConsoleService } from '../../services/nodeConsole.service';
|
||||||
|
|
||||||
|
|
||||||
@ -873,6 +874,18 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addNewTemplate() {
|
||||||
|
const dialogRef = this.dialog.open(NewTemplateDialogComponent, {
|
||||||
|
width: '1000px',
|
||||||
|
maxHeight: '500px',
|
||||||
|
autoFocus: false,
|
||||||
|
disableClose: true
|
||||||
|
});
|
||||||
|
let instance = dialogRef.componentInstance;
|
||||||
|
instance.server = this.server;
|
||||||
|
instance.project = this.project;
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.nodeConsoleService.openConsoles = 0;
|
this.nodeConsoleService.openConsoles = 0;
|
||||||
this.title.setTitle('GNS3 Web UI');
|
this.title.setTitle('GNS3 Web UI');
|
||||||
|
17
src/app/filters/dataSourceFilter.ts
Normal file
17
src/app/filters/dataSourceFilter.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Pipe, PipeTransform } from '@angular/core';
|
||||||
|
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'datasourcefilter'
|
||||||
|
})
|
||||||
|
export class DataSourceFilter implements PipeTransform {
|
||||||
|
transform(items: any, searchText: string): any[] {
|
||||||
|
if(!items) return [];
|
||||||
|
if(!searchText) return items;
|
||||||
|
|
||||||
|
searchText = searchText.toLowerCase();
|
||||||
|
return items.filteredData.filter( item => {
|
||||||
|
return item.name.toLowerCase().includes(searchText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -23,10 +23,12 @@ import {
|
|||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
MatTreeModule,
|
MatTreeModule,
|
||||||
MatBottomSheetModule,
|
MatBottomSheetModule,
|
||||||
MatChipsModule
|
MatChipsModule,
|
||||||
|
MatPaginatorModule
|
||||||
} from '@angular/material';
|
} from '@angular/material';
|
||||||
|
|
||||||
export const MATERIAL_IMPORTS = [
|
export const MATERIAL_IMPORTS = [
|
||||||
|
MatPaginatorModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
|
80
src/app/models/appliance.ts
Normal file
80
src/app/models/appliance.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
export interface Image {
|
||||||
|
download_url: string;
|
||||||
|
filename: string;
|
||||||
|
filesize: any;
|
||||||
|
md5sum: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Qemu {
|
||||||
|
adapter_type: string;
|
||||||
|
adapters: number;
|
||||||
|
arch: string;
|
||||||
|
boot_priority: string;
|
||||||
|
console_type: string;
|
||||||
|
hda_disk_interface: string;
|
||||||
|
kvm: string;
|
||||||
|
ram: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Docker {
|
||||||
|
adapters: number;
|
||||||
|
console_type: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Dynamips {
|
||||||
|
chassis: string;
|
||||||
|
nvram: number;
|
||||||
|
platform: string;
|
||||||
|
ram: number;
|
||||||
|
slot0: string;
|
||||||
|
startup_config: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Iou {
|
||||||
|
ethernet_adapters: number;
|
||||||
|
nvram: number;
|
||||||
|
ram: number;
|
||||||
|
serial_adapters: number;
|
||||||
|
startup_config: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Images {
|
||||||
|
hda_disk_image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Version {
|
||||||
|
images: Images;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Appliance {
|
||||||
|
availability: string;
|
||||||
|
builtin: boolean;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
documentation_url: string;
|
||||||
|
first_port_name: string;
|
||||||
|
images: Image[];
|
||||||
|
maintainer: string;
|
||||||
|
maintainer_email: string;
|
||||||
|
name: string;
|
||||||
|
port_name_format: string;
|
||||||
|
product_name: string;
|
||||||
|
product_url: string;
|
||||||
|
registry_version: number;
|
||||||
|
status: string;
|
||||||
|
symbol: string;
|
||||||
|
usage: string;
|
||||||
|
vendor_name: string;
|
||||||
|
vendor_url: string;
|
||||||
|
versions: Version[];
|
||||||
|
|
||||||
|
docker: Docker;
|
||||||
|
dynamips: Dynamips;
|
||||||
|
iou: Iou;
|
||||||
|
qemu: Qemu;
|
||||||
|
|
||||||
|
emulator?: string;
|
||||||
|
}
|
24
src/app/services/appliances.service.ts
Normal file
24
src/app/services/appliances.service.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpServer } from './http-server.service';
|
||||||
|
import { Server } from '../models/server';
|
||||||
|
import { Compute } from '../models/compute';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ComputeStatistics } from '../models/computeStatistics';
|
||||||
|
import { Appliance } from '../models/appliance';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApplianceService {
|
||||||
|
constructor(private httpServer: HttpServer) {}
|
||||||
|
|
||||||
|
getAppliances(server: Server): Observable<Appliance[]> {
|
||||||
|
return this.httpServer.get<Appliance[]>(server, '/appliances') as Observable<Appliance[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAppliance(server: Server, url): Observable<Appliance> {
|
||||||
|
return this.httpServer.get<Appliance>(server, url) as Observable<Appliance>;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUploadPath(server: Server, emulator: string, filename: string) {
|
||||||
|
return `http://${server.host}:${server.port}/v2/compute/${emulator}/images/${filename}`;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user