diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 25d5a0d5..bdda351c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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 { AlignVerticallyActionComponent } from './components/project-map/context-menu/actions/align_vertically/align-vertically.component'; import { ConfirmationBottomSheetComponent } from './components/projects/confirmation-bottomsheet/confirmation-bottomsheet.component'; +import { TemplateFilter } from './filters/templateFilter.pipe'; import { NotificationService } from './services/notification.service'; import { DeviceDetectorModule } from 'ngx-device-detector'; import { ConfigDialogComponent } from './components/project-map/context-menu/dialogs/config-dialog/config-dialog.component'; @@ -365,6 +366,7 @@ if (environment.production) { SearchFilter, DateFilter, NameFilter, + TemplateFilter, ProjectsFilter, ListOfSnapshotsComponent, CustomAdaptersComponent, diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index 0fa2d1b6..28d3cee4 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -111,6 +111,10 @@ + + + + + - + diff --git a/src/app/components/project-map/project-map.component.scss b/src/app/components/project-map/project-map.component.scss index 118c8972..e7458af0 100644 --- a/src/app/components/project-map/project-map.component.scss +++ b/src/app/components/project-map/project-map.component.scss @@ -111,7 +111,7 @@ mat-divider.divider { font-weight: bold; mat-icon { - margin-top: 8px; + margin-left: -6px; } } @@ -126,7 +126,7 @@ mat-divider.divider { font-weight: bold; mat-icon { - margin-top: 8px; + margin-left: -6px; } } } diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index aef53430..88c51fac 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -63,6 +63,7 @@ import { EthernetLinkWidget } from '../../cartography/widgets/links/ethernet-lin import { SerialLinkWidget } from '../../cartography/widgets/links/serial-link'; import { NavigationDialogComponent } from '../projects/navigation-dialog/navigation-dialog.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'; @@ -414,12 +415,15 @@ export class ProjectMapComponent implements OnInit, OnDestroy { this.mapChangeDetectorRef.detectChanges(); } - onNodeCreation(template: Template) { - if(!template) { + onNodeCreation(nodeAddedEvent: NodeAddedEvent) { + if(!nodeAddedEvent) { return; } - - this.nodeService.createFromTemplate(this.server, this.project, template, 0, 0, 'local').subscribe(() => { + this.nodeService.createFromTemplate(this.server, this.project, nodeAddedEvent.template, nodeAddedEvent.x, nodeAddedEvent.y, 'local').subscribe((node: Node) => { + 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[]) => { nodes.filter((node) => node.label.style === null).forEach((node) => { @@ -428,6 +432,12 @@ export class ProjectMapComponent implements OnInit, OnDestroy { }); 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); + } }); }); } diff --git a/src/app/components/template/template-list-dialog/template-list-dialog.component.html b/src/app/components/template/template-list-dialog/template-list-dialog.component.html index 3eea74e5..4306d9dd 100644 --- a/src/app/components/template/template-list-dialog/template-list-dialog.component.html +++ b/src/app/components/template/template-list-dialog/template-list-dialog.component.html @@ -1,22 +1,67 @@ +
+

Add a node

+ +
-
- - - +
+
Template
+ + + + + + + {{type}} + + + + + + + {{template.name}} + + + - - - Name - - {{ row.name }} - - +
+
Configuration
+
+
+ + + + + + +
- - -
+
+
Position
+
+
+ Left:  + + + + Top:  + + + +
+
diff --git a/src/app/components/template/template-list-dialog/template-list-dialog.component.scss b/src/app/components/template/template-list-dialog/template-list-dialog.component.scss index 6b2a2818..62ee0283 100644 --- a/src/app/components/template/template-list-dialog/template-list-dialog.component.scss +++ b/src/app/components/template/template-list-dialog/template-list-dialog.component.scss @@ -8,8 +8,8 @@ } .mat-table { - overflow: auto; - max-height: 400px; + height: 200px; + overflow: scroll; } .mat-form-field { @@ -17,11 +17,21 @@ flex-grow: 1; } +.form-field { + width: 100%; +} + div { scrollbar-color: darkgrey #263238; scrollbar-width: thin; } +h6 { + margin-top: 5px; + margin-bottom: 10px; + color: #0097a7; +} + mat-table { scrollbar-color: darkgrey #263238; scrollbar-width: thin; @@ -36,6 +46,17 @@ mat-table { } ::-webkit-scrollbar-thumb { -background-color: darkgrey; -outline: 1px solid #263238; + background-color: darkgrey; + outline: 1px solid #263238; +} + +.filterBox { + display: flex; + justify-content: space-between; +} + +.title-container { + display: flex; + align-items: baseline; + justify-content: space-between; } diff --git a/src/app/components/template/template-list-dialog/template-list-dialog.component.ts b/src/app/components/template/template-list-dialog/template-list-dialog.component.ts index 471305f3..37fa20e0 100644 --- a/src/app/components/template/template-list-dialog/template-list-dialog.component.ts +++ b/src/app/components/template/template-list-dialog/template-list-dialog.component.ts @@ -1,13 +1,14 @@ import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; import { DataSource } from '@angular/cdk/collections'; - import { Observable, BehaviorSubject, fromEvent, merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators'; - import { Server } from '../../../models/server'; import { TemplateService } from '../../../services/template.service'; 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({ selector: 'app-template-list-dialog', @@ -16,44 +17,88 @@ import { Template } from '../../../models/template'; }) export class TemplateListDialogComponent implements OnInit { server: Server; - templateDatabase: TemplateDatabase; - dataSource: TemplateDataSource; - displayedColumns = ['name']; - - @ViewChild('filter', {static: true}) filter: ElementRef; + project: Project; + templateTypes: string[] = ['cloud', 'ethernet_hub', 'ethernet_switch', 'docker', 'dynamips', 'vpcs', 'traceng', 'virtualbox', 'vmware', 'iou', 'qemu']; + selectedType: string; + configurationForm: FormGroup; + positionForm: FormGroup; + templates: Template[]; + filteredTemplates: Template[]; + selectedTemplate: Template; + searchText: string = ''; constructor( public dialogRef: MatDialogRef, 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.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() { - this.templateDatabase = new TemplateDatabase(this.server, this.templateService); - this.dataSource = new TemplateDataSource(this.templateDatabase); - - fromEvent(this.filter.nativeElement, 'keyup') - .pipe( - debounceTime(150), - distinctUntilChanged() - ) - .subscribe(() => { - if (!this.dataSource) { - return; - } - this.dataSource.filter = this.filter.nativeElement.value; - }); + this.templateService.list(this.server).subscribe((listOfTemplates: Template[]) => { + this.filteredTemplates = listOfTemplates; + this.templates = listOfTemplates; + }); } onNoClick(): void { this.dialogRef.close(); } - addNode(template: Template): void { - this.dialogRef.close(template); + filterTemplates(event) { + 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 { diff --git a/src/app/components/template/template.component.html b/src/app/components/template/template.component.html index c4fec0c6..a9535011 100644 --- a/src/app/components/template/template.component.html +++ b/src/app/components/template/template.component.html @@ -1 +1 @@ - + diff --git a/src/app/components/template/template.component.ts b/src/app/components/template/template.component.ts index 9b6675ca..8b833e40 100644 --- a/src/app/components/template/template.component.ts +++ b/src/app/components/template/template.component.ts @@ -1,9 +1,10 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 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 { Template } from '../../models/template'; +import { Project } from '../../models/project'; @Component({ selector: 'app-template', @@ -12,6 +13,7 @@ import { Template } from '../../models/template'; }) export class TemplateComponent implements OnInit { @Input() server: Server; + @Input() project: Project; @Output() onNodeCreation = new EventEmitter(); constructor(private dialog: MatDialog) {} @@ -21,16 +23,16 @@ export class TemplateComponent implements OnInit { listTemplatesModal() { const dialogRef = this.dialog.open(TemplateListDialogComponent, { width: '600px', - height: '560px', data: { - server: this.server + server: this.server, + project: this.project }, autoFocus: false }); - dialogRef.afterClosed().subscribe((template: Template) => { - if (template !== null) { - this.onNodeCreation.emit(template); + dialogRef.afterClosed().subscribe((nodeAddedEvent: NodeAddedEvent) => { + if (nodeAddedEvent !== null) { + this.onNodeCreation.emit(nodeAddedEvent); } }); } diff --git a/src/app/filters/templateFilter.pipe.ts b/src/app/filters/templateFilter.pipe.ts new file mode 100644 index 00000000..7c1e30a1 --- /dev/null +++ b/src/app/filters/templateFilter.pipe.ts @@ -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); + }); + } +} diff --git a/src/app/models/template.ts b/src/app/models/template.ts index 1dc03032..9a6ef23d 100644 --- a/src/app/models/template.ts +++ b/src/app/models/template.ts @@ -7,4 +7,5 @@ export class Template { name: string; node_type: string; symbol: string; + template_type: string; } diff --git a/src/app/services/node.service.ts b/src/app/services/node.service.ts index ff1b2bb6..2e8467ae 100644 --- a/src/app/services/node.service.ts +++ b/src/app/services/node.service.ts @@ -45,7 +45,7 @@ export class NodeService { 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 { return this.httpServer.post(server, `/projects/${project.project_id}/templates/${template.template_id}`, { x: Math.round(x), y: Math.round(y),