Merge pull request #596 from GNS3/Improvements-for-adding-nodes

Improvements for adding nodes
This commit is contained in:
piotrpekala7
2019-11-18 16:26:33 +01:00
committed by GitHub
12 changed files with 206 additions and 62 deletions

View File

@ -249,6 +249,7 @@ import { PageNotFoundComponent } from './components/page-not-found/page-not-foun
import { AlignHorizontallyActionComponent } from './components/project-map/context-menu/actions/align-horizontally/align-horizontally.component'; import { AlignHorizontallyActionComponent } from './components/project-map/context-menu/actions/align-horizontally/align-horizontally.component';
import { AlignVerticallyActionComponent } from './components/project-map/context-menu/actions/align_vertically/align-vertically.component'; import { AlignVerticallyActionComponent } from './components/project-map/context-menu/actions/align_vertically/align-vertically.component';
import { ConfirmationBottomSheetComponent } from './components/projects/confirmation-bottomsheet/confirmation-bottomsheet.component'; import { ConfirmationBottomSheetComponent } from './components/projects/confirmation-bottomsheet/confirmation-bottomsheet.component';
import { TemplateFilter } from './filters/templateFilter.pipe';
import { NotificationService } from './services/notification.service'; import { NotificationService } from './services/notification.service';
import { DeviceDetectorModule } from 'ngx-device-detector'; import { DeviceDetectorModule } from 'ngx-device-detector';
import { ConfigDialogComponent } from './components/project-map/context-menu/dialogs/config-dialog/config-dialog.component'; import { ConfigDialogComponent } from './components/project-map/context-menu/dialogs/config-dialog/config-dialog.component';
@ -365,6 +366,7 @@ if (environment.production) {
SearchFilter, SearchFilter,
DateFilter, DateFilter,
NameFilter, NameFilter,
TemplateFilter,
ProjectsFilter, ProjectsFilter,
ListOfSnapshotsComponent, ListOfSnapshotsComponent,
CustomAdaptersComponent, CustomAdaptersComponent,

View File

@ -111,6 +111,10 @@
</div> </div>
</mat-menu> </mat-menu>
<mat-toolbar-row *ngIf="!readonly">
<app-template [server]="server" [project]="project" (onNodeCreation)="onNodeCreation($event)"></app-template>
</mat-toolbar-row>
<mat-toolbar-row *ngIf="!readonly"> <mat-toolbar-row *ngIf="!readonly">
<button matTooltip="Add a link" mat-icon-button [color]="tools.draw_link ? 'primary' : 'basic'" (click)="toggleDrawLineMode()"> <button matTooltip="Add a link" mat-icon-button [color]="tools.draw_link ? 'primary' : 'basic'" (click)="toggleDrawLineMode()">
<mat-icon>timeline</mat-icon> <mat-icon>timeline</mat-icon>
@ -127,10 +131,6 @@
<app-snapshot-menu-item [server]="server" [project]="project"> </app-snapshot-menu-item> <app-snapshot-menu-item [server]="server" [project]="project"> </app-snapshot-menu-item>
</mat-toolbar-row> </mat-toolbar-row>
<mat-toolbar-row *ngIf="!readonly">
<app-template [server]="server" (onNodeCreation)="onNodeCreation($event)"></app-template>
</mat-toolbar-row>
<mat-toolbar-row *ngIf="!readonly"> <mat-toolbar-row *ngIf="!readonly">
<button matTooltip="Go to preferences" mat-icon-button routerLink="/server/{{server.id}}/preferences"> <button matTooltip="Go to preferences" mat-icon-button routerLink="/server/{{server.id}}/preferences">
<mat-icon>settings_applications</mat-icon> <mat-icon>settings_applications</mat-icon>
@ -160,9 +160,9 @@
</div> </div>
<div id="zoom-buttons"> <div id="zoom-buttons">
<button class="zoom-button" (click)="zoomIn()">+</button> <button class="zoom-button" (click)="zoomIn()"><mat-icon>zoom_in</mat-icon></button>
<button class="zoom-button" (click)="resetZoom()"><mat-icon>adjust</mat-icon></button> <button class="zoom-button" (click)="resetZoom()"><mat-icon>adjust</mat-icon></button>
<button class="zoom-button" (click)="zoomOut()">-</button> <button class="zoom-button" (click)="zoomOut()"><mat-icon>zoom_out</mat-icon></button>
</div> </div>
<app-progress></app-progress> <app-progress></app-progress>

View File

@ -111,7 +111,7 @@ mat-divider.divider {
font-weight: bold; font-weight: bold;
mat-icon { mat-icon {
margin-top: 8px; margin-left: -6px;
} }
} }
@ -126,7 +126,7 @@ mat-divider.divider {
font-weight: bold; font-weight: bold;
mat-icon { mat-icon {
margin-top: 8px; margin-left: -6px;
} }
} }
} }

View File

@ -63,6 +63,7 @@ import { EthernetLinkWidget } from '../../cartography/widgets/links/ethernet-lin
import { SerialLinkWidget } from '../../cartography/widgets/links/serial-link'; import { SerialLinkWidget } from '../../cartography/widgets/links/serial-link';
import { NavigationDialogComponent } from '../projects/navigation-dialog/navigation-dialog.component'; import { NavigationDialogComponent } from '../projects/navigation-dialog/navigation-dialog.component';
import { ConfirmationBottomSheetComponent } from '../projects/confirmation-bottomsheet/confirmation-bottomsheet.component'; import { ConfirmationBottomSheetComponent } from '../projects/confirmation-bottomsheet/confirmation-bottomsheet.component';
import { NodeAddedEvent } from '../template/template-list-dialog/template-list-dialog.component';
import { NotificationService } from '../../services/notification.service'; import { NotificationService } from '../../services/notification.service';
@ -414,12 +415,15 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
this.mapChangeDetectorRef.detectChanges(); this.mapChangeDetectorRef.detectChanges();
} }
onNodeCreation(template: Template) { onNodeCreation(nodeAddedEvent: NodeAddedEvent) {
if(!template) { if(!nodeAddedEvent) {
return; return;
} }
this.nodeService.createFromTemplate(this.server, this.project, nodeAddedEvent.template, nodeAddedEvent.x, nodeAddedEvent.y, 'local').subscribe((node: Node) => {
this.nodeService.createFromTemplate(this.server, this.project, template, 0, 0, 'local').subscribe(() => { if (nodeAddedEvent.name !== nodeAddedEvent.template.name) {
node.name = nodeAddedEvent.name;
this.nodeService.updateNode(this.server, node).subscribe(()=>{});
}
this.projectService.nodes(this.server, this.project.project_id).subscribe((nodes: Node[]) => { this.projectService.nodes(this.server, this.project.project_id).subscribe((nodes: Node[]) => {
nodes.filter((node) => node.label.style === null).forEach((node) => { nodes.filter((node) => node.label.style === null).forEach((node) => {
@ -428,6 +432,12 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
}); });
this.nodesDataSource.set(nodes); this.nodesDataSource.set(nodes);
nodeAddedEvent.numberOfNodes--;
if (nodeAddedEvent.numberOfNodes > 0) {
nodeAddedEvent.x = nodeAddedEvent.x + 50 < this.project.scene_width/2 ? nodeAddedEvent.x + 50 : nodeAddedEvent.x;
nodeAddedEvent.y = nodeAddedEvent.y + 50 < this.project.scene_height/2 ? nodeAddedEvent.y + 50 : nodeAddedEvent.y;
this.onNodeCreation(nodeAddedEvent);
}
}); });
}); });
} }

View File

@ -1,22 +1,67 @@
<div mat-dialog-content class="content"> <div class="title-container">
<div class="header"> <h1 mat-dialog-title>Add a node</h1>
<mat-form-field floatPlaceholder="never"> <button mat-button class="top-button" color="accent" (click)="onNoClick()" routerLink="/server/{{server.id}}/preferences">Go to template preferences</button>
<input matInput #filter placeholder="Filter templates" />
</mat-form-field>
</div> </div>
<div mat-dialog-content class="content">
<div class="title-container">
<h6>Template</h6>
</div>
<mat-form-field class="form-field" floatPlaceholder="never">
<input matInput
placeholder="Search by name"
[(ngModel)]="searchText"
[ngModelOptions]="{standalone: true}">
</mat-form-field>
<mat-form-field class="form-field">
<mat-select
[ngModelOptions]="{standalone: true}"
placeholder="Filter templates by type"
(selectionChange)="filterTemplates($event)"
[(ngModel)]="selectedType">
<mat-option *ngFor="let type of templateTypes" [value]="type">
{{type}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-select
[ngModelOptions]="{standalone: true}"
placeholder="Choose template"
(selectionChange)="chooseTemplate($event)"
[(ngModel)]="selectedTemplate">
<mat-option *ngFor="let template of filteredTemplates | templatefilter: searchText" [value]="template">
{{template.name}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-table #table [dataSource]="dataSource"> <div class="title-container">
<ng-container matColumnDef="name"> <h6>Configuration</h6>
<mat-header-cell *matHeaderCellDef> Name </mat-header-cell> </div>
<mat-cell *matCellDef="let row"> <form [formGroup]="configurationForm">
<a (click)="addNode(row)" href="javascript:void(0);" class="table-link">{{ row.name }}</a> <mat-form-field class="form-field">
</mat-cell> <input type="text" matInput formControlName="name" placeholder="Enter name (default is taken from template)" />
</ng-container> </mat-form-field>
<mat-form-field class="form-field">
<input type="number" matInput formControlName="numberOfNodes" placeholder="Enter number of nodes (default value is 1)" />
</mat-form-field>
</form>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <div class="title-container">
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> <h6>Position</h6>
</mat-table> </div>
<form [formGroup]="positionForm">
Left:&nbsp;
<mat-form-field>
<input matInput type="number" formControlName="left"/>
</mat-form-field>
Top:&nbsp;
<mat-form-field>
<input matInput type="number" formControlName="top"/>
</mat-form-field>
</form>
</div> </div>
<div mat-dialog-actions align="end"> <div mat-dialog-actions align="end">
<button mat-button (click)="onNoClick()" tabindex="-1" color="accent">Close</button> <button mat-button (click)="onNoClick()" tabindex="-1" color="accent">Close</button>
<button mat-button (click)="onAddClick()" tabindex="2" mat-raised-button color="primary">Add</button>
</div> </div>

View File

@ -8,8 +8,8 @@
} }
.mat-table { .mat-table {
overflow: auto; height: 200px;
max-height: 400px; overflow: scroll;
} }
.mat-form-field { .mat-form-field {
@ -17,11 +17,21 @@
flex-grow: 1; flex-grow: 1;
} }
.form-field {
width: 100%;
}
div { div {
scrollbar-color: darkgrey #263238; scrollbar-color: darkgrey #263238;
scrollbar-width: thin; scrollbar-width: thin;
} }
h6 {
margin-top: 5px;
margin-bottom: 10px;
color: #0097a7;
}
mat-table { mat-table {
scrollbar-color: darkgrey #263238; scrollbar-color: darkgrey #263238;
scrollbar-width: thin; scrollbar-width: thin;
@ -39,3 +49,14 @@ mat-table {
background-color: darkgrey; background-color: darkgrey;
outline: 1px solid #263238; outline: 1px solid #263238;
} }
.filterBox {
display: flex;
justify-content: space-between;
}
.title-container {
display: flex;
align-items: baseline;
justify-content: space-between;
}

View File

@ -1,13 +1,14 @@
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { DataSource } from '@angular/cdk/collections'; import { DataSource } from '@angular/cdk/collections';
import { Observable, BehaviorSubject, fromEvent, merge } from 'rxjs'; import { Observable, BehaviorSubject, fromEvent, merge } from 'rxjs';
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
import { Server } from '../../../models/server'; import { Server } from '../../../models/server';
import { TemplateService } from '../../../services/template.service'; import { TemplateService } from '../../../services/template.service';
import { Template } from '../../../models/template'; import { Template } from '../../../models/template';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
import { ToasterService } from '../../../services/toaster.service';
import { Project } from '../../../models/project';
@Component({ @Component({
selector: 'app-template-list-dialog', selector: 'app-template-list-dialog',
@ -16,34 +17,39 @@ import { Template } from '../../../models/template';
}) })
export class TemplateListDialogComponent implements OnInit { export class TemplateListDialogComponent implements OnInit {
server: Server; server: Server;
templateDatabase: TemplateDatabase; project: Project;
dataSource: TemplateDataSource; templateTypes: string[] = ['cloud', 'ethernet_hub', 'ethernet_switch', 'docker', 'dynamips', 'vpcs', 'traceng', 'virtualbox', 'vmware', 'iou', 'qemu'];
displayedColumns = ['name']; selectedType: string;
configurationForm: FormGroup;
@ViewChild('filter', {static: true}) filter: ElementRef; positionForm: FormGroup;
templates: Template[];
filteredTemplates: Template[];
selectedTemplate: Template;
searchText: string = '';
constructor( constructor(
public dialogRef: MatDialogRef<TemplateListDialogComponent>, public dialogRef: MatDialogRef<TemplateListDialogComponent>,
private templateService: TemplateService, private templateService: TemplateService,
@Inject(MAT_DIALOG_DATA) public data: any private formBuilder: FormBuilder,
@Inject(MAT_DIALOG_DATA) public data: any,
private toasterService: ToasterService
) { ) {
this.server = data['server']; this.server = data['server'];
this.project = data['project'];
this.configurationForm = this.formBuilder.group({
name: new FormControl('new node', Validators.required),
numberOfNodes: new FormControl(1, Validators.required)
});
this.positionForm = this.formBuilder.group({
top: new FormControl(0, Validators.required),
left: new FormControl(0, Validators.required)
});
} }
ngOnInit() { ngOnInit() {
this.templateDatabase = new TemplateDatabase(this.server, this.templateService); this.templateService.list(this.server).subscribe((listOfTemplates: Template[]) => {
this.dataSource = new TemplateDataSource(this.templateDatabase); this.filteredTemplates = listOfTemplates;
this.templates = listOfTemplates;
fromEvent(this.filter.nativeElement, 'keyup')
.pipe(
debounceTime(150),
distinctUntilChanged()
)
.subscribe(() => {
if (!this.dataSource) {
return;
}
this.dataSource.filter = this.filter.nativeElement.value;
}); });
} }
@ -51,9 +57,48 @@ export class TemplateListDialogComponent implements OnInit {
this.dialogRef.close(); this.dialogRef.close();
} }
addNode(template: Template): void { filterTemplates(event) {
this.dialogRef.close(template); let temporaryTemplates = this.templates.filter( item => {
return item.name.toLowerCase().includes(this.searchText.toLowerCase());
});
this.filteredTemplates = temporaryTemplates.filter(t => t.template_type === event.value.toString());
} }
chooseTemplate(event) {
this.selectedTemplate = event.value;
this.configurationForm.controls['name'].setValue(this.selectedTemplate.default_name_format);
}
onAddClick(): void {
if (!this.selectedTemplate || this.filteredTemplates.length === 0) {
this.toasterService.error('Please firstly choose template.');
} else if (!this.positionForm.valid || !this.configurationForm.valid) {
this.toasterService.error('Please fill all required fields.');
} else {
let x: number = this.positionForm.get('left').value;
let y: number = this.positionForm.get('top').value;
if (x>(this.project.scene_width/2) || x<-(this.project.scene_width/2) || y>(this.project.scene_height/2) || y<-(this.project.scene_height)) {
this.toasterService.error('Please set correct position values.')
} else {
let event: NodeAddedEvent = {
template: this.selectedTemplate,
name: this.configurationForm.get('name').value,
numberOfNodes: this.configurationForm.get('numberOfNodes').value,
x: x,
y: y
};
this.dialogRef.close(event);
}
}
}
}
export interface NodeAddedEvent {
template: Template,
name: string,
numberOfNodes: number;
x: number;
y: number;
} }
export class TemplateDatabase { export class TemplateDatabase {

View File

@ -1 +1 @@
<button matTooltip="Browse all devices" mat-icon-button (click)="listTemplatesModal()"><mat-icon>add_to_queue</mat-icon></button> <button matTooltip="Add a node" mat-icon-button (click)="listTemplatesModal()"><mat-icon>add_to_queue</mat-icon></button>

View File

@ -1,9 +1,10 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { MatDialog } from '@angular/material'; import { MatDialog } from '@angular/material';
import { TemplateListDialogComponent } from './template-list-dialog/template-list-dialog.component'; import { TemplateListDialogComponent, NodeAddedEvent } from './template-list-dialog/template-list-dialog.component';
import { Server } from '../../models/server'; import { Server } from '../../models/server';
import { Template } from '../../models/template'; import { Template } from '../../models/template';
import { Project } from '../../models/project';
@Component({ @Component({
selector: 'app-template', selector: 'app-template',
@ -12,6 +13,7 @@ import { Template } from '../../models/template';
}) })
export class TemplateComponent implements OnInit { export class TemplateComponent implements OnInit {
@Input() server: Server; @Input() server: Server;
@Input() project: Project;
@Output() onNodeCreation = new EventEmitter<any>(); @Output() onNodeCreation = new EventEmitter<any>();
constructor(private dialog: MatDialog) {} constructor(private dialog: MatDialog) {}
@ -21,16 +23,16 @@ export class TemplateComponent implements OnInit {
listTemplatesModal() { listTemplatesModal() {
const dialogRef = this.dialog.open(TemplateListDialogComponent, { const dialogRef = this.dialog.open(TemplateListDialogComponent, {
width: '600px', width: '600px',
height: '560px',
data: { data: {
server: this.server server: this.server,
project: this.project
}, },
autoFocus: false autoFocus: false
}); });
dialogRef.afterClosed().subscribe((template: Template) => { dialogRef.afterClosed().subscribe((nodeAddedEvent: NodeAddedEvent) => {
if (template !== null) { if (nodeAddedEvent !== null) {
this.onNodeCreation.emit(template); this.onNodeCreation.emit(nodeAddedEvent);
} }
}); });
} }

View File

@ -0,0 +1,18 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Template } from '../models/template';
@Pipe({
name: 'templatefilter'
})
export class TemplateFilter implements PipeTransform {
transform(items: Template[], searchText: string): any[] {
if(!items) return [];
if(!searchText) return items;
searchText = searchText.toLowerCase();
return items.filter( item => {
return item.name.toLowerCase().includes(searchText);
});
}
}

View File

@ -7,4 +7,5 @@ export class Template {
name: string; name: string;
node_type: string; node_type: string;
symbol: string; symbol: string;
template_type: string;
} }

View File

@ -45,7 +45,7 @@ export class NodeService {
return this.httpServer.post(server, `/projects/${project.project_id}/nodes/reload`, {}); return this.httpServer.post(server, `/projects/${project.project_id}/nodes/reload`, {});
} }
createFromTemplate(server: Server, project: Project, template: Template, x: number, y: number, compute_id: string) { createFromTemplate(server: Server, project: Project, template: Template, x: number, y: number, compute_id: string): Observable<Node> {
return this.httpServer.post(server, `/projects/${project.project_id}/templates/${template.template_id}`, { return this.httpServer.post(server, `/projects/${project.project_id}/templates/${template.template_id}`, {
x: Math.round(x), x: Math.round(x),
y: Math.round(y), y: Math.round(y),