mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-05-11 04:52:59 +00:00
Merge branch 'develop' into adding-blank-project
This commit is contained in:
commit
5cb520f39d
@ -47,6 +47,7 @@
|
|||||||
"d3-ng2-service": "^2.1.0",
|
"d3-ng2-service": "^2.1.0",
|
||||||
"electron-settings": "^3.2.0",
|
"electron-settings": "^3.2.0",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
|
"ng2-file-upload": "^1.3.0",
|
||||||
"ngx-electron": "^1.0.4",
|
"ngx-electron": "^1.0.4",
|
||||||
"notosans-fontface": "^1.1.0",
|
"notosans-fontface": "^1.1.0",
|
||||||
"npm-check-updates": "^2.14.1",
|
"npm-check-updates": "^2.14.1",
|
||||||
@ -62,10 +63,10 @@
|
|||||||
"@angular/cli": "^6.0.8",
|
"@angular/cli": "^6.0.8",
|
||||||
"@angular/compiler-cli": "^6.0.7",
|
"@angular/compiler-cli": "^6.0.7",
|
||||||
"@angular/language-service": "^6.0.7",
|
"@angular/language-service": "^6.0.7",
|
||||||
|
"@sentry/electron": "^0.7.0",
|
||||||
"@types/jasmine": "~2.8.8",
|
"@types/jasmine": "~2.8.8",
|
||||||
"@types/jasminewd2": "~2.0.2",
|
"@types/jasminewd2": "~2.0.2",
|
||||||
"@types/node": "~10.5.2",
|
"@types/node": "~10.5.2",
|
||||||
"@sentry/electron": "^0.7.0",
|
|
||||||
"codelyzer": "~4.4.2",
|
"codelyzer": "~4.4.2",
|
||||||
"electron": "2.0.4",
|
"electron": "2.0.4",
|
||||||
"electron-builder": "^20.19.2",
|
"electron-builder": "^20.19.2",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as Raven from 'raven-js';
|
import * as Raven from 'raven-js';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { NgModule, ErrorHandler } from '@angular/core';
|
import { NgModule, ErrorHandler } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { CdkTableModule } from "@angular/cdk/table";
|
import { CdkTableModule } from "@angular/cdk/table";
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { HotkeyModule } from 'angular2-hotkeys';
|
import { HotkeyModule } from 'angular2-hotkeys';
|
||||||
import { PersistenceModule } from 'angular-persistence';
|
import { PersistenceModule } from 'angular-persistence';
|
||||||
import { NgxElectronModule } from 'ngx-electron';
|
import { NgxElectronModule } from 'ngx-electron';
|
||||||
|
import { FileUploadModule } from 'ng2-file-upload';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
|
|
||||||
import { VersionService } from './services/version.service';
|
import { VersionService } from './services/version.service';
|
||||||
@ -28,6 +28,8 @@ import { ApplianceService } from "./services/appliance.service";
|
|||||||
import { LinkService } from "./services/link.service";
|
import { LinkService } from "./services/link.service";
|
||||||
|
|
||||||
import { ProjectsComponent } from './components/projects/projects.component';
|
import { ProjectsComponent } from './components/projects/projects.component';
|
||||||
|
import { ImportProjectDialogComponent } from './components/projects/import-project-dialog/import-project-dialog.component';
|
||||||
|
import { ImportProjectConfirmationDialogComponent} from './components/projects/import-project-dialog/import-project-confirmation-dialog/import-project-confirmation-dialog.component';
|
||||||
import { DefaultLayoutComponent } from './layouts/default-layout/default-layout.component';
|
import { DefaultLayoutComponent } from './layouts/default-layout/default-layout.component';
|
||||||
import { ProgressDialogComponent } from './common/progress-dialog/progress-dialog.component';
|
import { ProgressDialogComponent } from './common/progress-dialog/progress-dialog.component';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
@ -67,6 +69,7 @@ import { CreateSnapshotDialogComponent } from './components/snapshots/create-sna
|
|||||||
import { SnapshotsComponent } from './components/snapshots/snapshots.component';
|
import { SnapshotsComponent } from './components/snapshots/snapshots.component';
|
||||||
import { SnapshotMenuItemComponent } from './components/snapshots/snapshot-menu-item/snapshot-menu-item.component';
|
import { SnapshotMenuItemComponent } from './components/snapshots/snapshot-menu-item/snapshot-menu-item.component';
|
||||||
import { MATERIAL_IMPORTS } from './material.imports';
|
import { MATERIAL_IMPORTS } from './material.imports';
|
||||||
|
import { DrawingService } from './services/drawing.service';
|
||||||
|
|
||||||
|
|
||||||
if (environment.production) {
|
if (environment.production) {
|
||||||
@ -91,6 +94,8 @@ if (environment.production) {
|
|||||||
SnapshotMenuItemComponent,
|
SnapshotMenuItemComponent,
|
||||||
SnapshotsComponent,
|
SnapshotsComponent,
|
||||||
ProjectsComponent,
|
ProjectsComponent,
|
||||||
|
ImportProjectDialogComponent,
|
||||||
|
ImportProjectConfirmationDialogComponent,
|
||||||
DefaultLayoutComponent,
|
DefaultLayoutComponent,
|
||||||
ProgressDialogComponent,
|
ProgressDialogComponent,
|
||||||
NodeContextMenuComponent,
|
NodeContextMenuComponent,
|
||||||
@ -112,13 +117,15 @@ if (environment.production) {
|
|||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
CdkTableModule,
|
CdkTableModule,
|
||||||
CartographyModule,
|
CartographyModule,
|
||||||
HotkeyModule.forRoot(),
|
HotkeyModule.forRoot(),
|
||||||
PersistenceModule,
|
PersistenceModule,
|
||||||
NgxElectronModule,
|
NgxElectronModule,
|
||||||
...MATERIAL_IMPORTS
|
FileUploadModule,
|
||||||
|
MATERIAL_IMPORTS
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SettingsService,
|
SettingsService,
|
||||||
@ -131,6 +138,7 @@ if (environment.production) {
|
|||||||
ApplianceService,
|
ApplianceService,
|
||||||
NodeService,
|
NodeService,
|
||||||
LinkService,
|
LinkService,
|
||||||
|
DrawingService,
|
||||||
IndexedDbService,
|
IndexedDbService,
|
||||||
HttpServer,
|
HttpServer,
|
||||||
SnapshotService,
|
SnapshotService,
|
||||||
@ -151,7 +159,9 @@ if (environment.production) {
|
|||||||
AddServerDialogComponent,
|
AddServerDialogComponent,
|
||||||
CreateSnapshotDialogComponent,
|
CreateSnapshotDialogComponent,
|
||||||
ProgressDialogComponent,
|
ProgressDialogComponent,
|
||||||
ApplianceListDialogComponent
|
ApplianceListDialogComponent,
|
||||||
|
ImportProjectDialogComponent,
|
||||||
|
ImportProjectConfirmationDialogComponent
|
||||||
],
|
],
|
||||||
bootstrap: [ AppComponent ]
|
bootstrap: [ AppComponent ]
|
||||||
})
|
})
|
||||||
|
@ -16,6 +16,11 @@ import { MapChangeDetectorRef } from './services/map-change-detector-ref';
|
|||||||
import { Context } from './models/context';
|
import { Context } from './models/context';
|
||||||
import { D3_MAP_IMPORTS } from './d3-map.imports';
|
import { D3_MAP_IMPORTS } from './d3-map.imports';
|
||||||
import { CanvasSizeDetector } from './helpers/canvas-size-detector';
|
import { CanvasSizeDetector } from './helpers/canvas-size-detector';
|
||||||
|
import { MapListeners } from './listeners/map-listeners';
|
||||||
|
import { DrawingsDraggableListener } from './listeners/drawings-draggable-listener';
|
||||||
|
import { NodesDraggableListener } from './listeners/nodes-draggable-listener';
|
||||||
|
import { DrawingsEventSource } from './events/drawings-event-source';
|
||||||
|
import { NodesEventSource } from './events/nodes-event-source';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -39,6 +44,11 @@ import { CanvasSizeDetector } from './helpers/canvas-size-detector';
|
|||||||
MapChangeDetectorRef,
|
MapChangeDetectorRef,
|
||||||
CanvasSizeDetector,
|
CanvasSizeDetector,
|
||||||
Context,
|
Context,
|
||||||
|
MapListeners,
|
||||||
|
DrawingsDraggableListener,
|
||||||
|
NodesDraggableListener,
|
||||||
|
DrawingsEventSource,
|
||||||
|
NodesEventSource,
|
||||||
...D3_MAP_IMPORTS
|
...D3_MAP_IMPORTS
|
||||||
],
|
],
|
||||||
exports: [ MapComponent ]
|
exports: [ MapComponent ]
|
||||||
|
@ -2,11 +2,11 @@ import { Component, OnInit, Output, EventEmitter, OnDestroy, ViewChild } from '@
|
|||||||
import { Port } from '../../../models/port';
|
import { Port } from '../../../models/port';
|
||||||
import { DrawingLineWidget } from '../../widgets/drawing-line';
|
import { DrawingLineWidget } from '../../widgets/drawing-line';
|
||||||
import { Node } from '../../models/node';
|
import { Node } from '../../models/node';
|
||||||
import { NodesWidget } from '../../widgets/nodes';
|
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { NodeSelectInterfaceComponent } from '../node-select-interface/node-select-interface.component';
|
import { NodeSelectInterfaceComponent } from '../node-select-interface/node-select-interface.component';
|
||||||
import { LinkCreated } from '../../events/links';
|
import { LinkCreated } from '../../events/links';
|
||||||
import { NodeClicked } from '../../events/nodes';
|
import { NodeClicked } from '../../events/nodes';
|
||||||
|
import { NodeWidget } from '../../widgets/node';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -23,11 +23,11 @@ export class DrawLinkToolComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private drawingLineTool: DrawingLineWidget,
|
private drawingLineTool: DrawingLineWidget,
|
||||||
private nodesWidget: NodesWidget
|
private nodeWidget: NodeWidget
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.onNodeClicked = this.nodesWidget.onNodeClicked.subscribe((eventNode: NodeClicked) => {
|
this.onNodeClicked = this.nodeWidget.onNodeClicked.subscribe((eventNode: NodeClicked) => {
|
||||||
this.nodeSelectInterfaceMenu.open(
|
this.nodeSelectInterfaceMenu.open(
|
||||||
eventNode.node,
|
eventNode.node,
|
||||||
eventNode.event.clientY,
|
eventNode.event.clientY,
|
||||||
|
@ -17,9 +17,15 @@ import { SelectionTool } from '../../tools/selection-tool';
|
|||||||
import { MovingTool } from '../../tools/moving-tool';
|
import { MovingTool } from '../../tools/moving-tool';
|
||||||
import { LinksWidget } from '../../widgets/links';
|
import { LinksWidget } from '../../widgets/links';
|
||||||
import { MapChangeDetectorRef } from '../../services/map-change-detector-ref';
|
import { MapChangeDetectorRef } from '../../services/map-change-detector-ref';
|
||||||
import { NodeDragging, NodeDragged } from '../../events/nodes';
|
import { NodeDragging, NodeDragged, NodeClicked } from '../../events/nodes';
|
||||||
import { LinkCreated } from '../../events/links';
|
import { LinkCreated } from '../../events/links';
|
||||||
import { CanvasSizeDetector } from '../../helpers/canvas-size-detector';
|
import { CanvasSizeDetector } from '../../helpers/canvas-size-detector';
|
||||||
|
import { NodeWidget } from '../../widgets/node';
|
||||||
|
import { MapListeners } from '../../listeners/map-listeners';
|
||||||
|
import { DraggedDataEvent } from '../../events/event-source';
|
||||||
|
import { NodesEventSource } from '../../events/nodes-event-source';
|
||||||
|
import { DrawingsEventSource } from '../../events/drawings-event-source';
|
||||||
|
import { DrawingsWidget } from '../../widgets/drawings';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -36,13 +42,13 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
@Input() width = 1500;
|
@Input() width = 1500;
|
||||||
@Input() height = 600;
|
@Input() height = 600;
|
||||||
|
|
||||||
@Output() onNodeDragged: EventEmitter<NodeDragged>;
|
@Output() nodeDragged: EventEmitter<DraggedDataEvent<Node>>;
|
||||||
|
@Output() drawingDragged: EventEmitter<DraggedDataEvent<Drawing>>;
|
||||||
@Output() onLinkCreated = new EventEmitter<LinkCreated>();
|
@Output() onLinkCreated = new EventEmitter<LinkCreated>();
|
||||||
|
|
||||||
private parentNativeElement: any;
|
private parentNativeElement: any;
|
||||||
private svg: Selection<SVGSVGElement, any, null, undefined>;
|
private svg: Selection<SVGSVGElement, any, null, undefined>;
|
||||||
|
|
||||||
private onNodeDraggingSubscription: Subscription;
|
|
||||||
private onChangesDetected: Subscription;
|
private onChangesDetected: Subscription;
|
||||||
|
|
||||||
protected settings = {
|
protected settings = {
|
||||||
@ -53,16 +59,22 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
private context: Context,
|
private context: Context,
|
||||||
private mapChangeDetectorRef: MapChangeDetectorRef,
|
private mapChangeDetectorRef: MapChangeDetectorRef,
|
||||||
private canvasSizeDetector: CanvasSizeDetector,
|
private canvasSizeDetector: CanvasSizeDetector,
|
||||||
|
private mapListeners: MapListeners,
|
||||||
protected element: ElementRef,
|
protected element: ElementRef,
|
||||||
protected nodesWidget: NodesWidget,
|
protected nodesWidget: NodesWidget,
|
||||||
|
protected nodeWidget: NodeWidget,
|
||||||
protected linksWidget: LinksWidget,
|
protected linksWidget: LinksWidget,
|
||||||
|
protected drawingsWidget: DrawingsWidget,
|
||||||
protected interfaceLabelWidget: InterfaceLabelWidget,
|
protected interfaceLabelWidget: InterfaceLabelWidget,
|
||||||
protected selectionToolWidget: SelectionTool,
|
protected selectionToolWidget: SelectionTool,
|
||||||
protected movingToolWidget: MovingTool,
|
protected movingToolWidget: MovingTool,
|
||||||
public graphLayout: GraphLayout
|
public graphLayout: GraphLayout,
|
||||||
|
nodesEventSource: NodesEventSource,
|
||||||
|
drawingsEventSource: DrawingsEventSource,
|
||||||
) {
|
) {
|
||||||
this.parentNativeElement = element.nativeElement;
|
this.parentNativeElement = element.nativeElement;
|
||||||
this.onNodeDragged = nodesWidget.onNodeDragged;
|
this.nodeDragged = nodesEventSource.dragged;
|
||||||
|
this.drawingDragged = drawingsEventSource.dragged;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input('show-interface-labels')
|
@Input('show-interface-labels')
|
||||||
@ -85,6 +97,11 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Input('draw-link-tool') drawLinkTool: boolean;
|
@Input('draw-link-tool') drawLinkTool: boolean;
|
||||||
|
|
||||||
|
@Input('readonly') set readonly(value) {
|
||||||
|
this.nodesWidget.draggingEnabled = !value;
|
||||||
|
this.drawingsWidget.draggingEnabled == !value;
|
||||||
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||||
if (
|
if (
|
||||||
@ -110,31 +127,25 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.graphLayout.disconnect(this.svg);
|
|
||||||
this.onNodeDraggingSubscription.unsubscribe();
|
|
||||||
this.onChangesDetected.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.parentNativeElement !== null) {
|
if (this.parentNativeElement !== null) {
|
||||||
this.createGraph(this.parentNativeElement);
|
this.createGraph(this.parentNativeElement);
|
||||||
}
|
}
|
||||||
this.context.size = this.getSize();
|
this.context.size = this.getSize();
|
||||||
|
|
||||||
this.onNodeDraggingSubscription = this.nodesWidget.onNodeDragging.subscribe((eventNode: NodeDragging) => {
|
|
||||||
const links = this.links.filter((link) => link.target.node_id === eventNode.node.node_id || link.source.node_id === eventNode.node.node_id);
|
|
||||||
|
|
||||||
links.forEach((link) => {
|
|
||||||
this.linksWidget.redrawLink(this.svg, link);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onChangesDetected = this.mapChangeDetectorRef.changesDetected.subscribe(() => {
|
this.onChangesDetected = this.mapChangeDetectorRef.changesDetected.subscribe(() => {
|
||||||
if (this.mapChangeDetectorRef.hasBeenDrawn) {
|
if (this.mapChangeDetectorRef.hasBeenDrawn) {
|
||||||
this.reload();
|
this.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.mapListeners.onInit(this.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.graphLayout.disconnect(this.svg);
|
||||||
|
this.onChangesDetected.unsubscribe();
|
||||||
|
this.mapListeners.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
public createGraph(domElement: HTMLElement) {
|
public createGraph(domElement: HTMLElement) {
|
||||||
@ -193,7 +204,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onSymbolsChange(change: SimpleChange) {
|
private onSymbolsChange(change: SimpleChange) {
|
||||||
this.graphLayout.getNodesWidget().setSymbols(this.symbols);
|
this.nodeWidget.setSymbols(this.symbols);
|
||||||
}
|
}
|
||||||
|
|
||||||
public redraw() {
|
public redraw() {
|
||||||
|
@ -14,11 +14,14 @@ import { ImageDrawingWidget } from './widgets/drawings/image-drawing';
|
|||||||
import { RectDrawingWidget } from './widgets/drawings/rect-drawing';
|
import { RectDrawingWidget } from './widgets/drawings/rect-drawing';
|
||||||
import { TextDrawingWidget } from './widgets/drawings/text-drawing';
|
import { TextDrawingWidget } from './widgets/drawings/text-drawing';
|
||||||
import { LineDrawingWidget } from './widgets/drawings/line-drawing';
|
import { LineDrawingWidget } from './widgets/drawings/line-drawing';
|
||||||
|
import { NodeWidget } from './widgets/node';
|
||||||
|
import { DrawingWidget } from './widgets/drawing';
|
||||||
|
|
||||||
export const D3_MAP_IMPORTS = [
|
export const D3_MAP_IMPORTS = [
|
||||||
GraphLayout,
|
GraphLayout,
|
||||||
LinksWidget,
|
LinksWidget,
|
||||||
NodesWidget,
|
NodesWidget,
|
||||||
|
NodeWidget,
|
||||||
DrawingsWidget,
|
DrawingsWidget,
|
||||||
DrawingLineWidget,
|
DrawingLineWidget,
|
||||||
SelectionTool,
|
SelectionTool,
|
||||||
@ -32,4 +35,5 @@ export const D3_MAP_IMPORTS = [
|
|||||||
LineDrawingWidget,
|
LineDrawingWidget,
|
||||||
RectDrawingWidget,
|
RectDrawingWidget,
|
||||||
TextDrawingWidget,
|
TextDrawingWidget,
|
||||||
|
DrawingWidget
|
||||||
];
|
];
|
||||||
|
72
src/app/cartography/events/draggable.ts
Normal file
72
src/app/cartography/events/draggable.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { EventEmitter } from "@angular/core";
|
||||||
|
import { drag, DraggedElementBaseType } from "d3-drag";
|
||||||
|
import { event } from "d3-selection";
|
||||||
|
|
||||||
|
class DraggableEvent {
|
||||||
|
public x: number;
|
||||||
|
public y: number;
|
||||||
|
public dx: number;
|
||||||
|
public dy: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DraggableStart<T> extends DraggableEvent {
|
||||||
|
constructor(
|
||||||
|
public datum: T
|
||||||
|
){
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DraggableDrag<T> extends DraggableEvent {
|
||||||
|
constructor(
|
||||||
|
public datum: T
|
||||||
|
){
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DraggableEnd<T> extends DraggableEvent {
|
||||||
|
constructor(
|
||||||
|
public datum: T
|
||||||
|
){
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Draggable<GElement extends DraggedElementBaseType, Datum> {
|
||||||
|
public start = new EventEmitter<DraggableStart<Datum>>();
|
||||||
|
public drag = new EventEmitter<DraggableStart<Datum>>();
|
||||||
|
public end = new EventEmitter<DraggableStart<Datum>>();
|
||||||
|
|
||||||
|
public call(selection) {
|
||||||
|
selection.call(this.behaviour());
|
||||||
|
}
|
||||||
|
|
||||||
|
private behaviour() {
|
||||||
|
return drag<GElement, Datum>()
|
||||||
|
.on('start', (datum: Datum) => {
|
||||||
|
const evt = new DraggableStart<Datum>(datum);
|
||||||
|
evt.dx = event.dx;
|
||||||
|
evt.dy = event.dy;
|
||||||
|
evt.x = event.x;
|
||||||
|
evt.y = event.y;
|
||||||
|
this.start.emit(evt);
|
||||||
|
})
|
||||||
|
.on('drag', (datum: Datum) => {
|
||||||
|
const evt = new DraggableDrag<Datum>(datum);
|
||||||
|
evt.dx = event.dx;
|
||||||
|
evt.dy = event.dy;
|
||||||
|
evt.x = event.x;
|
||||||
|
evt.y = event.y;
|
||||||
|
this.drag.emit(evt);
|
||||||
|
})
|
||||||
|
.on('end', (datum: Datum) => {
|
||||||
|
const evt = new DraggableEnd<Datum>(datum);
|
||||||
|
evt.dx = event.dx;
|
||||||
|
evt.dy = event.dy;
|
||||||
|
evt.x = event.x;
|
||||||
|
evt.y = event.y;
|
||||||
|
this.end.emit(evt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
9
src/app/cartography/events/drawings-event-source.ts
Normal file
9
src/app/cartography/events/drawings-event-source.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Injectable, EventEmitter } from "@angular/core";
|
||||||
|
import { Drawing } from "../models/drawing";
|
||||||
|
import { DraggedDataEvent } from "./event-source";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DrawingsEventSource {
|
||||||
|
public dragged = new EventEmitter<DraggedDataEvent<Drawing>>();
|
||||||
|
}
|
9
src/app/cartography/events/event-source.ts
Normal file
9
src/app/cartography/events/event-source.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export class DataEventSource<T> {
|
||||||
|
constructor(
|
||||||
|
public datum: T
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// class CreatedDataEvent<T> extends DataEventSource<T> {}
|
||||||
|
export class DraggedDataEvent<T> extends DataEventSource<T> {}
|
9
src/app/cartography/events/nodes-event-source.ts
Normal file
9
src/app/cartography/events/nodes-event-source.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Injectable, EventEmitter } from "@angular/core";
|
||||||
|
import { Node } from "../models/node";
|
||||||
|
import { DraggedDataEvent } from "./event-source";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NodesEventSource {
|
||||||
|
public dragged = new EventEmitter<DraggedDataEvent<Node>>();
|
||||||
|
}
|
55
src/app/cartography/listeners/drawings-draggable-listener.ts
Normal file
55
src/app/cartography/listeners/drawings-draggable-listener.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { DrawingsWidget } from "../widgets/drawings";
|
||||||
|
import { DraggableStart } from "../events/draggable";
|
||||||
|
import { Drawing } from "../models/drawing";
|
||||||
|
import { Subscription } from "rxjs";
|
||||||
|
import { SelectionManager } from "../managers/selection-manager";
|
||||||
|
import { DrawingsEventSource } from "../events/drawings-event-source";
|
||||||
|
import { DraggedDataEvent } from "../events/event-source";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DrawingsDraggableListener {
|
||||||
|
private start: Subscription;
|
||||||
|
private drag: Subscription;
|
||||||
|
private end: Subscription;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private drawingsWidget: DrawingsWidget,
|
||||||
|
private selectionManager: SelectionManager,
|
||||||
|
private drawingsEventSource: DrawingsEventSource
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public onInit(svg: any) {
|
||||||
|
this.start = this.drawingsWidget.draggable.start.subscribe((evt: DraggableStart<Drawing>) => {
|
||||||
|
let drawings = this.selectionManager.getSelectedDrawings();
|
||||||
|
if (drawings.filter((n: Drawing) => n.drawing_id === evt.datum.drawing_id).length === 0) {
|
||||||
|
this.selectionManager.setSelectedDrawings([evt.datum]);
|
||||||
|
drawings = this.selectionManager.getSelectedDrawings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.drag = this.drawingsWidget.draggable.drag.subscribe((evt: DraggableStart<Drawing>) => {
|
||||||
|
let drawings = this.selectionManager.getSelectedDrawings();
|
||||||
|
drawings.forEach((drawing: Drawing) => {
|
||||||
|
drawing.x += evt.dx;
|
||||||
|
drawing.y += evt.dy;
|
||||||
|
this.drawingsWidget.redrawDrawing(svg, drawing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.end = this.drawingsWidget.draggable.end.subscribe((evt: DraggableStart<Drawing>) => {
|
||||||
|
let drawings = this.selectionManager.getSelectedDrawings();
|
||||||
|
drawings.forEach((drawing: Drawing) => {
|
||||||
|
this.drawingsEventSource.dragged.emit(new DraggedDataEvent<Drawing>(drawing));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDestroy() {
|
||||||
|
this.start.unsubscribe();
|
||||||
|
this.drag.unsubscribe();
|
||||||
|
this.end.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
4
src/app/cartography/listeners/map-listener.ts
Normal file
4
src/app/cartography/listeners/map-listener.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface MapListener {
|
||||||
|
onInit(svg: any);
|
||||||
|
onDestroy();
|
||||||
|
}
|
29
src/app/cartography/listeners/map-listeners.ts
Normal file
29
src/app/cartography/listeners/map-listeners.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { MapListener } from "./map-listener";
|
||||||
|
import { DrawingsDraggableListener } from "./drawings-draggable-listener";
|
||||||
|
import { NodesDraggableListener } from "./nodes-draggable-listener";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MapListeners {
|
||||||
|
private listeners: MapListener[] = [];
|
||||||
|
constructor(
|
||||||
|
private drawingsDraggableListener: DrawingsDraggableListener,
|
||||||
|
private nodesDraggableListener: NodesDraggableListener
|
||||||
|
) {
|
||||||
|
this.listeners.push(this.drawingsDraggableListener);
|
||||||
|
this.listeners.push(this.nodesDraggableListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onInit(svg: any) {
|
||||||
|
this.listeners.forEach((listener) => {
|
||||||
|
listener.onInit(svg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDestroy() {
|
||||||
|
this.listeners.forEach((listener) => {
|
||||||
|
listener.onDestroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
64
src/app/cartography/listeners/nodes-draggable-listener.ts
Normal file
64
src/app/cartography/listeners/nodes-draggable-listener.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { NodesWidget } from "../widgets/nodes";
|
||||||
|
import { DraggableStart } from "../events/draggable";
|
||||||
|
import { Node } from "../models/node";
|
||||||
|
import { Subscription } from "rxjs";
|
||||||
|
import { SelectionManager } from "../managers/selection-manager";
|
||||||
|
import { LinksWidget } from "../widgets/links";
|
||||||
|
import { GraphLayout } from "../widgets/graph-layout";
|
||||||
|
import { NodesEventSource } from "../events/nodes-event-source";
|
||||||
|
import { DraggedDataEvent } from "../events/event-source";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NodesDraggableListener {
|
||||||
|
private start: Subscription;
|
||||||
|
private drag: Subscription;
|
||||||
|
private end: Subscription;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private nodesWidget: NodesWidget,
|
||||||
|
private linksWidget: LinksWidget,
|
||||||
|
private selectionManager: SelectionManager,
|
||||||
|
private graphLayout: GraphLayout,
|
||||||
|
private nodesEventSource: NodesEventSource
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public onInit(svg: any) {
|
||||||
|
this.start = this.nodesWidget.draggable.start.subscribe((evt: DraggableStart<Node>) => {
|
||||||
|
let nodes = this.selectionManager.getSelectedNodes();
|
||||||
|
if (nodes.filter((n: Node) => n.node_id === evt.datum.node_id).length === 0) {
|
||||||
|
this.selectionManager.setSelectedNodes([evt.datum]);
|
||||||
|
nodes = this.selectionManager.getSelectedNodes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.drag = this.nodesWidget.draggable.drag.subscribe((evt: DraggableStart<Node>) => {
|
||||||
|
let nodes = this.selectionManager.getSelectedNodes();
|
||||||
|
nodes.forEach((node: Node) => {
|
||||||
|
node.x += evt.dx;
|
||||||
|
node.y += evt.dy;
|
||||||
|
this.nodesWidget.redrawNode(svg, node);
|
||||||
|
|
||||||
|
const links = this.graphLayout.getLinks().filter((link) => link.target.node_id === node.node_id || link.source.node_id === node.node_id);
|
||||||
|
links.forEach((link) => {
|
||||||
|
this.linksWidget.redrawLink(svg, link);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.end = this.nodesWidget.draggable.end.subscribe((evt: DraggableStart<Node>) => {
|
||||||
|
let nodes = this.selectionManager.getSelectedNodes();
|
||||||
|
nodes.forEach((node: Node) => {
|
||||||
|
this.nodesEventSource.dragged.emit(new DraggedDataEvent<Node>(node));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDestroy() {
|
||||||
|
this.start.unsubscribe();
|
||||||
|
this.drag.unsubscribe();
|
||||||
|
this.end.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
52
src/app/cartography/widgets/drawing.ts
Normal file
52
src/app/cartography/widgets/drawing.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { Widget } from "./widget";
|
||||||
|
import { SVGSelection } from "../models/types";
|
||||||
|
import { Drawing } from "../models/drawing";
|
||||||
|
import { DrawingShapeWidget } from "./drawings/drawing-shape-widget";
|
||||||
|
import { TextDrawingWidget } from "./drawings/text-drawing";
|
||||||
|
import { ImageDrawingWidget } from "./drawings/image-drawing";
|
||||||
|
import { RectDrawingWidget } from "./drawings/rect-drawing";
|
||||||
|
import { LineDrawingWidget } from "./drawings/line-drawing";
|
||||||
|
import { EllipseDrawingWidget } from "./drawings/ellipse-drawing";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DrawingWidget implements Widget {
|
||||||
|
private drawingWidgets: DrawingShapeWidget[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private textDrawingWidget: TextDrawingWidget,
|
||||||
|
private imageDrawingWidget: ImageDrawingWidget,
|
||||||
|
private rectDrawingWidget: RectDrawingWidget,
|
||||||
|
private lineDrawingWidget: LineDrawingWidget,
|
||||||
|
private ellipseDrawingWidget: EllipseDrawingWidget
|
||||||
|
) {
|
||||||
|
this.drawingWidgets = [
|
||||||
|
this.textDrawingWidget,
|
||||||
|
this.imageDrawingWidget,
|
||||||
|
this.rectDrawingWidget,
|
||||||
|
this.lineDrawingWidget,
|
||||||
|
this.ellipseDrawingWidget
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public draw(view: SVGSelection) {
|
||||||
|
const drawing_body = view.selectAll<SVGGElement, Drawing>("g.drawing_body")
|
||||||
|
.data((l) => [l]);
|
||||||
|
|
||||||
|
const drawing_body_enter = drawing_body.enter()
|
||||||
|
.append<SVGGElement>('g')
|
||||||
|
.attr("class", "drawing_body");
|
||||||
|
|
||||||
|
const drawing_body_merge = drawing_body.merge(drawing_body_enter)
|
||||||
|
.attr('transform', (d: Drawing) => {
|
||||||
|
return `translate(${d.x},${d.y}) rotate(${d.rotation})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.drawingWidgets.forEach((widget) => {
|
||||||
|
widget.draw(drawing_body_merge);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -4,70 +4,67 @@ import { Widget } from "./widget";
|
|||||||
import { Drawing } from "../models/drawing";
|
import { Drawing } from "../models/drawing";
|
||||||
import { SVGSelection } from "../models/types";
|
import { SVGSelection } from "../models/types";
|
||||||
import { Layer } from "../models/layer";
|
import { Layer } from "../models/layer";
|
||||||
import { TextDrawingWidget } from "./drawings/text-drawing";
|
|
||||||
import { SvgToDrawingConverter } from "../helpers/svg-to-drawing-converter";
|
import { SvgToDrawingConverter } from "../helpers/svg-to-drawing-converter";
|
||||||
import { ImageDrawingWidget } from "./drawings/image-drawing";
|
import { Draggable } from "../events/draggable";
|
||||||
import { RectDrawingWidget } from "./drawings/rect-drawing";
|
import { DrawingWidget } from "./drawing";
|
||||||
import { LineDrawingWidget } from "./drawings/line-drawing";
|
|
||||||
import { EllipseDrawingWidget } from "./drawings/ellipse-drawing";
|
|
||||||
import { DrawingWidget } from "./drawings/drawing-widget";
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DrawingsWidget implements Widget {
|
export class DrawingsWidget implements Widget {
|
||||||
private drawingWidgets: DrawingWidget[] = [];
|
public draggable = new Draggable<SVGGElement, Drawing>();
|
||||||
|
public draggingEnabled = false;
|
||||||
|
|
||||||
|
// public onContextMenu = new EventEmitter<NodeContextMenu>();
|
||||||
|
// public onDrawingClicked = new EventEmitter<NodeClicked>();
|
||||||
|
// public onDrawingDragged = new EventEmitter<NodeDragged>();
|
||||||
|
// public onDrawingDragging = new EventEmitter<NodeDragging>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private drawingWidget: DrawingWidget,
|
||||||
private svgToDrawingConverter: SvgToDrawingConverter,
|
private svgToDrawingConverter: SvgToDrawingConverter,
|
||||||
private textDrawingWidget: TextDrawingWidget,
|
|
||||||
private imageDrawingWidget: ImageDrawingWidget,
|
|
||||||
private rectDrawingWidget: RectDrawingWidget,
|
|
||||||
private lineDrawingWidget: LineDrawingWidget,
|
|
||||||
private ellipseDrawingWidget: EllipseDrawingWidget
|
|
||||||
) {
|
) {
|
||||||
this.svgToDrawingConverter = new SvgToDrawingConverter();
|
this.svgToDrawingConverter = new SvgToDrawingConverter();
|
||||||
|
|
||||||
this.drawingWidgets = [
|
|
||||||
this.textDrawingWidget,
|
|
||||||
this.imageDrawingWidget,
|
|
||||||
this.rectDrawingWidget,
|
|
||||||
this.lineDrawingWidget,
|
|
||||||
this.ellipseDrawingWidget
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public draw(view: SVGSelection, drawings?: Drawing[]) {
|
public redrawDrawing(view: SVGSelection, drawing: Drawing) {
|
||||||
|
this.drawingWidget.draw(this.selectDrawing(view, drawing));
|
||||||
|
}
|
||||||
|
|
||||||
|
public draw(view: SVGSelection) {
|
||||||
const drawing = view
|
const drawing = view
|
||||||
.selectAll<SVGGElement, Drawing>('g.drawing')
|
.selectAll<SVGGElement, Drawing>("g.drawing")
|
||||||
.data((l: Layer) => {
|
.data((layer: Layer) => {
|
||||||
l.drawings.forEach((d: Drawing) => {
|
layer.drawings.forEach((d: Drawing) => {
|
||||||
try {
|
try {
|
||||||
d.element = this.svgToDrawingConverter.convert(d.svg);
|
d.element = this.svgToDrawingConverter.convert(d.svg);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Cannot convert due to Error: '${error}'`);
|
console.log(`Cannot convert due to Error: '${error}'`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return l.drawings;
|
return layer.drawings;
|
||||||
}, (d: Drawing) => {
|
}, (l: Drawing) => {
|
||||||
return d.drawing_id;
|
return l.drawing_id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const drawing_enter = drawing.enter()
|
const drawing_enter = drawing.enter()
|
||||||
.append<SVGGElement>('g')
|
.append<SVGGElement>('g')
|
||||||
.attr('class', 'drawing');
|
.attr('class', 'drawing')
|
||||||
|
.attr('drawing_id', (l: Drawing) => l.drawing_id)
|
||||||
|
|
||||||
const drawing_merge = drawing.merge(drawing_enter)
|
const merge = drawing.merge(drawing_enter);
|
||||||
.attr('transform', (d: Drawing) => {
|
|
||||||
return `translate(${d.x},${d.y}) rotate(${d.rotation})`;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.drawingWidgets.forEach((widget) => {
|
this.drawingWidget.draw(merge);
|
||||||
widget.draw(drawing_merge);
|
|
||||||
});
|
|
||||||
|
|
||||||
drawing
|
drawing
|
||||||
.exit()
|
.exit()
|
||||||
.remove();
|
.remove();
|
||||||
|
|
||||||
|
if (this.draggingEnabled) {
|
||||||
|
this.draggable.call(merge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectDrawing(view: SVGSelection, drawing: Drawing) {
|
||||||
|
return view.selectAll<SVGGElement, Drawing>(`g.drawing[drawing_id="${drawing.drawing_id}"]`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { SVGSelection } from "../../models/types";
|
import { SVGSelection } from "../../models/types";
|
||||||
|
|
||||||
export interface DrawingWidget {
|
export interface DrawingShapeWidget {
|
||||||
draw(view: SVGSelection);
|
draw(view: SVGSelection);
|
||||||
}
|
}
|
@ -3,12 +3,12 @@ import { Injectable } from "@angular/core";
|
|||||||
import { SVGSelection } from "../../models/types";
|
import { SVGSelection } from "../../models/types";
|
||||||
import { Drawing } from "../../models/drawing";
|
import { Drawing } from "../../models/drawing";
|
||||||
import { EllipseElement } from "../../models/drawings/ellipse-element";
|
import { EllipseElement } from "../../models/drawings/ellipse-element";
|
||||||
import { DrawingWidget } from "./drawing-widget";
|
import { DrawingShapeWidget } from "./drawing-shape-widget";
|
||||||
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
|
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EllipseDrawingWidget implements DrawingWidget {
|
export class EllipseDrawingWidget implements DrawingShapeWidget {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private qtDasharrayFixer: QtDasharrayFixer
|
private qtDasharrayFixer: QtDasharrayFixer
|
||||||
|
@ -3,11 +3,11 @@ import { Injectable } from "@angular/core";
|
|||||||
import { SVGSelection } from "../../models/types";
|
import { SVGSelection } from "../../models/types";
|
||||||
import { Drawing } from "../../models/drawing";
|
import { Drawing } from "../../models/drawing";
|
||||||
import { ImageElement } from "../../models/drawings/image-element";
|
import { ImageElement } from "../../models/drawings/image-element";
|
||||||
import { DrawingWidget } from "./drawing-widget";
|
import { DrawingShapeWidget } from "./drawing-shape-widget";
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ImageDrawingWidget implements DrawingWidget {
|
export class ImageDrawingWidget implements DrawingShapeWidget {
|
||||||
public draw(view: SVGSelection) {
|
public draw(view: SVGSelection) {
|
||||||
const drawing = view
|
const drawing = view
|
||||||
.selectAll<SVGImageElement, ImageElement>('image.image_element')
|
.selectAll<SVGImageElement, ImageElement>('image.image_element')
|
||||||
|
@ -3,12 +3,12 @@ import { Injectable } from "@angular/core";
|
|||||||
import { SVGSelection } from "../../models/types";
|
import { SVGSelection } from "../../models/types";
|
||||||
import { Drawing } from "../../models/drawing";
|
import { Drawing } from "../../models/drawing";
|
||||||
import { LineElement } from "../../models/drawings/line-element";
|
import { LineElement } from "../../models/drawings/line-element";
|
||||||
import { DrawingWidget } from "./drawing-widget";
|
import { DrawingShapeWidget } from "./drawing-shape-widget";
|
||||||
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
|
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LineDrawingWidget implements DrawingWidget {
|
export class LineDrawingWidget implements DrawingShapeWidget {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private qtDasharrayFixer: QtDasharrayFixer
|
private qtDasharrayFixer: QtDasharrayFixer
|
||||||
|
@ -3,12 +3,12 @@ import { Injectable } from "@angular/core";
|
|||||||
import { SVGSelection } from "../../models/types";
|
import { SVGSelection } from "../../models/types";
|
||||||
import { Drawing } from "../../models/drawing";
|
import { Drawing } from "../../models/drawing";
|
||||||
import { RectElement } from "../../models/drawings/rect-element";
|
import { RectElement } from "../../models/drawings/rect-element";
|
||||||
import { DrawingWidget } from "./drawing-widget";
|
import { DrawingShapeWidget } from "./drawing-shape-widget";
|
||||||
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
|
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RectDrawingWidget implements DrawingWidget {
|
export class RectDrawingWidget implements DrawingShapeWidget {
|
||||||
constructor(
|
constructor(
|
||||||
private qtDasharrayFixer: QtDasharrayFixer
|
private qtDasharrayFixer: QtDasharrayFixer
|
||||||
) {}
|
) {}
|
||||||
|
@ -3,13 +3,13 @@ import { Injectable } from "@angular/core";
|
|||||||
import { SVGSelection } from "../../models/types";
|
import { SVGSelection } from "../../models/types";
|
||||||
import { TextElement } from "../../models/drawings/text-element";
|
import { TextElement } from "../../models/drawings/text-element";
|
||||||
import { Drawing } from "../../models/drawing";
|
import { Drawing } from "../../models/drawing";
|
||||||
import { DrawingWidget } from "./drawing-widget";
|
import { DrawingShapeWidget } from "./drawing-shape-widget";
|
||||||
import { FontFixer } from "../../helpers/font-fixer";
|
import { FontFixer } from "../../helpers/font-fixer";
|
||||||
import { select } from "d3-selection";
|
import { select } from "d3-selection";
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TextDrawingWidget implements DrawingWidget {
|
export class TextDrawingWidget implements DrawingShapeWidget {
|
||||||
static MARGIN = 4;
|
static MARGIN = 4;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -36,6 +36,10 @@ export class GraphLayout implements Widget {
|
|||||||
this.links = links;
|
this.links = links;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getLinks() {
|
||||||
|
return this.links;
|
||||||
|
}
|
||||||
|
|
||||||
public setDrawings(drawings: Drawing[]) {
|
public setDrawings(drawings: Drawing[]) {
|
||||||
this.drawings = drawings;
|
this.drawings = drawings;
|
||||||
}
|
}
|
||||||
|
@ -19,14 +19,6 @@ export class LinkWidget implements Widget {
|
|||||||
private interfaceStatusWidget: InterfaceStatusWidget
|
private interfaceStatusWidget: InterfaceStatusWidget
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public getInterfaceLabelWidget() {
|
|
||||||
return this.interfaceLabelWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getInterfaceStatusWidget() {
|
|
||||||
return this.interfaceStatusWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
public draw(view: SVGSelection) {
|
public draw(view: SVGSelection) {
|
||||||
const link_body = view.selectAll<SVGGElement, Link>("g.link_body")
|
const link_body = view.selectAll<SVGGElement, Link>("g.link_body")
|
||||||
.data((l) => [l]);
|
.data((l) => [l]);
|
||||||
@ -51,8 +43,8 @@ export class LinkWidget implements Widget {
|
|||||||
.select<SVGPathElement>('path')
|
.select<SVGPathElement>('path')
|
||||||
.classed('selected', (l: Link) => l.is_selected);
|
.classed('selected', (l: Link) => l.is_selected);
|
||||||
|
|
||||||
this.getInterfaceLabelWidget().draw(link_body_merge);
|
this.interfaceLabelWidget.draw(link_body_merge);
|
||||||
this.getInterfaceStatusWidget().draw(link_body_merge);
|
this.interfaceStatusWidget.draw(link_body_merge);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,12 +16,12 @@ describe('LinksWidget', () => {
|
|||||||
let widget: LinksWidget;
|
let widget: LinksWidget;
|
||||||
let layersEnter: Selection<SVGGElement, Layer, SVGGElement, any>;
|
let layersEnter: Selection<SVGGElement, Layer, SVGGElement, any>;
|
||||||
let layer: Layer;
|
let layer: Layer;
|
||||||
let mockedLinkWidget: LinkWidget;
|
let linkWidget: LinkWidget;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
svg = new TestSVGCanvas();
|
svg = new TestSVGCanvas();
|
||||||
mockedLinkWidget = instance(mock(LinkWidget));
|
linkWidget = instance(mock(LinkWidget));
|
||||||
widget = new LinksWidget(new MultiLinkCalculatorHelper(), mockedLinkWidget);
|
widget = new LinksWidget(new MultiLinkCalculatorHelper(), linkWidget);
|
||||||
|
|
||||||
const node_1 = new Node();
|
const node_1 = new Node();
|
||||||
node_1.node_id = "1";
|
node_1.node_id = "1";
|
||||||
@ -65,10 +65,6 @@ describe('LinksWidget', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should draw links', () => {
|
it('should draw links', () => {
|
||||||
const linkWidgetMock = mock(LinkWidget);
|
|
||||||
const linkWidget = instance(linkWidgetMock);
|
|
||||||
spyOn(widget, 'getLinkWidget').and.returnValue(linkWidget);
|
|
||||||
|
|
||||||
widget.draw(layersEnter);
|
widget.draw(layersEnter);
|
||||||
|
|
||||||
const drew = svg.canvas.selectAll<SVGGElement, Link>('g.link');
|
const drew = svg.canvas.selectAll<SVGGElement, Link>('g.link');
|
||||||
|
@ -11,16 +11,12 @@ import { LinkWidget } from "./link";
|
|||||||
export class LinksWidget implements Widget {
|
export class LinksWidget implements Widget {
|
||||||
constructor(
|
constructor(
|
||||||
private multiLinkCalculatorHelper: MultiLinkCalculatorHelper,
|
private multiLinkCalculatorHelper: MultiLinkCalculatorHelper,
|
||||||
private linkWidget: LinkWidget,
|
private linkWidget: LinkWidget
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLinkWidget() {
|
|
||||||
return this.linkWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
public redrawLink(view: SVGSelection, link: Link) {
|
public redrawLink(view: SVGSelection, link: Link) {
|
||||||
this.getLinkWidget().draw(this.selectLink(view, link));
|
this.linkWidget.draw(this.selectLink(view, link));
|
||||||
}
|
}
|
||||||
|
|
||||||
public draw(view: SVGSelection) {
|
public draw(view: SVGSelection) {
|
||||||
@ -48,7 +44,7 @@ export class LinksWidget implements Widget {
|
|||||||
|
|
||||||
const merge = link.merge(link_enter);
|
const merge = link.merge(link_enter);
|
||||||
|
|
||||||
this.getLinkWidget().draw(merge);
|
this.linkWidget.draw(merge);
|
||||||
|
|
||||||
link
|
link
|
||||||
.exit()
|
.exit()
|
||||||
|
70
src/app/cartography/widgets/node.spec.ts
Normal file
70
src/app/cartography/widgets/node.spec.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
import { TestSVGCanvas } from "../testing";
|
||||||
|
import { Node } from "../models/node";
|
||||||
|
import { Label } from "../models/label";
|
||||||
|
import { CssFixer } from "../helpers/css-fixer";
|
||||||
|
import { FontFixer } from "../helpers/font-fixer";
|
||||||
|
import { NodeWidget } from "./node";
|
||||||
|
|
||||||
|
|
||||||
|
describe('NodesWidget', () => {
|
||||||
|
let svg: TestSVGCanvas;
|
||||||
|
let widget: NodeWidget;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
svg = new TestSVGCanvas();
|
||||||
|
widget = new NodeWidget(new CssFixer(), new FontFixer());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
svg.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('draggable behaviour', () => {
|
||||||
|
let node: Node;
|
||||||
|
const tryToDrag = () => {
|
||||||
|
const drew = svg.canvas.selectAll<SVGGElement, Node>('g.node');
|
||||||
|
const drewNode = drew.nodes()[0];
|
||||||
|
|
||||||
|
drewNode.dispatchEvent(
|
||||||
|
new MouseEvent('mousedown', {
|
||||||
|
clientX: 150, clientY: 250, relatedTarget: drewNode,
|
||||||
|
screenY: 1024, screenX: 1024, view: window
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
window.dispatchEvent(new MouseEvent('mousemove', {clientX: 300, clientY: 300}));
|
||||||
|
window.dispatchEvent(new MouseEvent('mouseup', {clientX: 300, clientY: 300, view: window}));
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
node = new Node();
|
||||||
|
node.x = 100;
|
||||||
|
node.y = 200;
|
||||||
|
node.width = 100;
|
||||||
|
node.height = 100;
|
||||||
|
node.label = new Label();
|
||||||
|
});
|
||||||
|
|
||||||
|
// it('should be draggable when enabled', () => {
|
||||||
|
// widget.setDraggingEnabled(true);
|
||||||
|
// widget.draw(svg.canvas);
|
||||||
|
|
||||||
|
// tryToDrag();
|
||||||
|
|
||||||
|
// expect(node.x).toEqual(250);
|
||||||
|
// expect(node.y).toEqual(250);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should be not draggable when disabled', () => {
|
||||||
|
// widget.setDraggingEnabled(false);
|
||||||
|
// widget.draw(svg.canvas);
|
||||||
|
|
||||||
|
// tryToDrag();
|
||||||
|
|
||||||
|
// expect(node.x).toEqual(100);
|
||||||
|
// expect(node.y).toEqual(200);
|
||||||
|
// });
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
117
src/app/cartography/widgets/node.ts
Normal file
117
src/app/cartography/widgets/node.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { Injectable, EventEmitter } from "@angular/core";
|
||||||
|
|
||||||
|
import { Widget } from "./widget";
|
||||||
|
import { SVGSelection } from "../models/types";
|
||||||
|
import { Node } from "../models/node";
|
||||||
|
import { NodeContextMenu, NodeClicked, NodeDragged, NodeDragging } from "../events/nodes";
|
||||||
|
import { CssFixer } from "../helpers/css-fixer";
|
||||||
|
import { FontFixer } from "../helpers/font-fixer";
|
||||||
|
import { select, event } from "d3-selection";
|
||||||
|
import { Symbol } from "../../models/symbol";
|
||||||
|
import { D3DragEvent, drag } from "d3-drag";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NodeWidget implements Widget {
|
||||||
|
static NODE_LABEL_MARGIN = 3;
|
||||||
|
|
||||||
|
public onContextMenu = new EventEmitter<NodeContextMenu>();
|
||||||
|
public onNodeClicked = new EventEmitter<NodeClicked>();
|
||||||
|
public onNodeDragged = new EventEmitter<NodeDragged>();
|
||||||
|
public onNodeDragging = new EventEmitter<NodeDragging>();
|
||||||
|
|
||||||
|
private symbols: Symbol[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cssFixer: CssFixer,
|
||||||
|
private fontFixer: FontFixer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public setSymbols(symbols: Symbol[]) {
|
||||||
|
this.symbols = symbols;
|
||||||
|
}
|
||||||
|
|
||||||
|
public draw(view: SVGSelection) {
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
const node_body = view.selectAll<SVGGElement, Node>("g.node_body")
|
||||||
|
.data((n) => [n]);
|
||||||
|
|
||||||
|
const node_body_enter = node_body.enter()
|
||||||
|
.append<SVGGElement>('g')
|
||||||
|
.attr("class", "node_body");
|
||||||
|
|
||||||
|
node_body_enter
|
||||||
|
.append<SVGImageElement>('image');
|
||||||
|
|
||||||
|
// add label of node
|
||||||
|
node_body_enter
|
||||||
|
.append<SVGTextElement>('text')
|
||||||
|
.attr('class', 'label');
|
||||||
|
|
||||||
|
const node_body_merge = node_body.merge(node_body_enter)
|
||||||
|
.classed('selected', (n: Node) => n.is_selected)
|
||||||
|
.on("contextmenu", function (n: Node, i: number) {
|
||||||
|
event.preventDefault();
|
||||||
|
self.onContextMenu.emit(new NodeContextMenu(event, n));
|
||||||
|
})
|
||||||
|
.on('click', (n: Node) => {
|
||||||
|
this.onNodeClicked.emit(new NodeClicked(event, n));
|
||||||
|
});
|
||||||
|
|
||||||
|
// update image of node
|
||||||
|
node_body_merge
|
||||||
|
.select<SVGImageElement>('image')
|
||||||
|
.attr('xnode:href', (n: Node) => {
|
||||||
|
const symbol = this.symbols.find((s: Symbol) => s.symbol_id === n.symbol);
|
||||||
|
if (symbol) {
|
||||||
|
return 'data:image/svg+xml;base64,' + btoa(symbol.raw);
|
||||||
|
}
|
||||||
|
// @todo; we need to have default image
|
||||||
|
return 'data:image/svg+xml;base64,none';
|
||||||
|
})
|
||||||
|
.attr('width', (n: Node) => n.width)
|
||||||
|
.attr('height', (n: Node) => n.height)
|
||||||
|
.attr('x', (n: Node) => 0)
|
||||||
|
.attr('y', (n: Node) => 0)
|
||||||
|
.on('mouseover', function (this, n: Node) {
|
||||||
|
select(this).attr("class", "over");
|
||||||
|
})
|
||||||
|
.on('mouseout', function (this, n: Node) {
|
||||||
|
select(this).attr("class", "");
|
||||||
|
});
|
||||||
|
|
||||||
|
node_body_merge
|
||||||
|
.attr('transform', (n: Node) => {
|
||||||
|
return `translate(${n.x},${n.y})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
node_body_merge
|
||||||
|
.select<SVGTextElement>('text.label')
|
||||||
|
// .attr('y', (n: Node) => n.label.y - n.height / 2. + 10) // @todo: server computes y in auto way
|
||||||
|
.attr('style', (n: Node) => {
|
||||||
|
let styles = this.cssFixer.fix(n.label.style);
|
||||||
|
styles = this.fontFixer.fixStyles(styles);
|
||||||
|
return styles;
|
||||||
|
})
|
||||||
|
.text((n: Node) => n.label.text)
|
||||||
|
.attr('x', function (this: SVGTextElement, n: Node) {
|
||||||
|
if (n.label.x === null) {
|
||||||
|
// center
|
||||||
|
const bbox = this.getBBox();
|
||||||
|
return -bbox.width / 2.;
|
||||||
|
}
|
||||||
|
return n.label.x + NodeWidget.NODE_LABEL_MARGIN;
|
||||||
|
})
|
||||||
|
.attr('y', function (this: SVGTextElement, n: Node) {
|
||||||
|
let bbox = this.getBBox();
|
||||||
|
|
||||||
|
if (n.label.x === null) {
|
||||||
|
// center
|
||||||
|
bbox = this.getBBox();
|
||||||
|
return - n.height / 2. - bbox.height ;
|
||||||
|
}
|
||||||
|
return n.label.y + bbox.height - NodeWidget.NODE_LABEL_MARGIN;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,74 +1,23 @@
|
|||||||
|
|
||||||
import { TestSVGCanvas } from "../testing";
|
import { TestSVGCanvas } from "../testing";
|
||||||
import { NodesWidget } from "./nodes";
|
import { NodesWidget } from "./nodes";
|
||||||
import { Node } from "../models/node";
|
import { NodeWidget } from "./node";
|
||||||
import { Label } from "../models/label";
|
import { instance, mock } from "ts-mockito";
|
||||||
import { CssFixer } from "../helpers/css-fixer";
|
|
||||||
import { FontFixer } from "../helpers/font-fixer";
|
|
||||||
|
|
||||||
|
|
||||||
describe('NodesWidget', () => {
|
describe('NodesWidget', () => {
|
||||||
let svg: TestSVGCanvas;
|
let svg: TestSVGCanvas;
|
||||||
|
let nodeWidget: NodeWidget;
|
||||||
let widget: NodesWidget;
|
let widget: NodesWidget;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
svg = new TestSVGCanvas();
|
svg = new TestSVGCanvas();
|
||||||
widget = new NodesWidget(
|
nodeWidget = instance(mock(NodeWidget));
|
||||||
new CssFixer(),
|
widget = new NodesWidget(nodeWidget);
|
||||||
new FontFixer()
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
svg.destroy();
|
svg.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('draggable behaviour', () => {
|
|
||||||
let node: Node;
|
|
||||||
const tryToDrag = () => {
|
|
||||||
const drew = svg.canvas.selectAll<SVGGElement, Node>('g.node');
|
|
||||||
const drewNode = drew.nodes()[0];
|
|
||||||
|
|
||||||
drewNode.dispatchEvent(
|
|
||||||
new MouseEvent('mousedown', {
|
|
||||||
clientX: 150, clientY: 250, relatedTarget: drewNode,
|
|
||||||
screenY: 1024, screenX: 1024, view: window
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
window.dispatchEvent(new MouseEvent('mousemove', {clientX: 300, clientY: 300}));
|
|
||||||
window.dispatchEvent(new MouseEvent('mouseup', {clientX: 300, clientY: 300, view: window}));
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
node = new Node();
|
|
||||||
node.x = 100;
|
|
||||||
node.y = 200;
|
|
||||||
node.width = 100;
|
|
||||||
node.height = 100;
|
|
||||||
node.label = new Label();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be draggable when enabled', () => {
|
|
||||||
widget.setDraggingEnabled(true);
|
|
||||||
widget.draw(svg.canvas, [node]);
|
|
||||||
|
|
||||||
tryToDrag();
|
|
||||||
|
|
||||||
expect(node.x).toEqual(250);
|
|
||||||
expect(node.y).toEqual(250);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be not draggable when disabled', () => {
|
|
||||||
widget.setDraggingEnabled(false);
|
|
||||||
widget.draw(svg.canvas, [node]);
|
|
||||||
|
|
||||||
tryToDrag();
|
|
||||||
|
|
||||||
expect(node.x).toEqual(100);
|
|
||||||
expect(node.y).toEqual(200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,190 +1,61 @@
|
|||||||
import { Injectable, EventEmitter } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
import { event, select, Selection } from "d3-selection";
|
|
||||||
import { D3DragEvent, drag } from "d3-drag";
|
|
||||||
|
|
||||||
import { Widget } from "./widget";
|
import { Widget } from "./widget";
|
||||||
import { Node } from "../models/node";
|
import { Node } from "../models/node";
|
||||||
import { SVGSelection } from "../models/types";
|
import { SVGSelection } from "../models/types";
|
||||||
import { Symbol } from "../../models/symbol";
|
|
||||||
import { Layer } from "../models/layer";
|
import { Layer } from "../models/layer";
|
||||||
import { CssFixer } from "../helpers/css-fixer";
|
import { NodeWidget } from "./node";
|
||||||
import { FontFixer } from "../helpers/font-fixer";
|
import { Draggable } from "../events/draggable";
|
||||||
import { NodeDragging, NodeDragged, NodeContextMenu, NodeClicked } from "../events/nodes";
|
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NodesWidget implements Widget {
|
export class NodesWidget implements Widget {
|
||||||
static NODE_LABEL_MARGIN = 3;
|
static NODE_LABEL_MARGIN = 3;
|
||||||
|
|
||||||
private debug = false;
|
public draggable = new Draggable<SVGGElement, Node>();
|
||||||
private draggingEnabled = false;
|
public draggingEnabled = false;
|
||||||
|
|
||||||
private symbols: Symbol[] = [];
|
|
||||||
|
|
||||||
public onContextMenu = new EventEmitter<NodeContextMenu>();
|
|
||||||
public onNodeClicked = new EventEmitter<NodeClicked>();
|
|
||||||
public onNodeDragged = new EventEmitter<NodeDragged>();
|
|
||||||
public onNodeDragging = new EventEmitter<NodeDragging>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cssFixer: CssFixer,
|
private nodeWidget: NodeWidget
|
||||||
private fontFixer: FontFixer
|
|
||||||
) {
|
) {
|
||||||
this.symbols = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public setSymbols(symbols: Symbol[]) {
|
public redrawNode(view: SVGSelection, node: Node) {
|
||||||
this.symbols = symbols;
|
this.nodeWidget.draw(this.selectNode(view, node));
|
||||||
}
|
}
|
||||||
|
|
||||||
public setDraggingEnabled(enabled: boolean) {
|
public draw(view: SVGSelection) {
|
||||||
this.draggingEnabled = enabled;
|
const node = view
|
||||||
}
|
.selectAll<SVGGElement, Node>("g.node")
|
||||||
|
.data((layer: Layer) => {
|
||||||
public revise(selection: SVGSelection) {
|
if (layer.nodes) {
|
||||||
selection
|
return layer.nodes;
|
||||||
.attr('transform', (n: Node) => {
|
}
|
||||||
return `translate(${n.x},${n.y})`;
|
return [];
|
||||||
});
|
|
||||||
|
|
||||||
selection
|
|
||||||
.select<SVGTextElement>('text.label')
|
|
||||||
// .attr('y', (n: Node) => n.label.y - n.height / 2. + 10) // @todo: server computes y in auto way
|
|
||||||
.attr('style', (n: Node) => {
|
|
||||||
let styles = this.cssFixer.fix(n.label.style);
|
|
||||||
styles = this.fontFixer.fixStyles(styles);
|
|
||||||
return styles;
|
|
||||||
})
|
|
||||||
.text((n: Node) => n.label.text)
|
|
||||||
.attr('x', function (this: SVGTextElement, n: Node) {
|
|
||||||
if (n.label.x === null) {
|
|
||||||
// center
|
|
||||||
const bbox = this.getBBox();
|
|
||||||
return -bbox.width / 2.;
|
|
||||||
}
|
|
||||||
return n.label.x + NodesWidget.NODE_LABEL_MARGIN;
|
|
||||||
})
|
|
||||||
.attr('y', function (this: SVGTextElement, n: Node) {
|
|
||||||
let bbox = this.getBBox();
|
|
||||||
|
|
||||||
if (n.label.x === null) {
|
|
||||||
// center
|
|
||||||
bbox = this.getBBox();
|
|
||||||
return - n.height / 2. - bbox.height ;
|
|
||||||
}
|
|
||||||
return n.label.y + bbox.height - NodesWidget.NODE_LABEL_MARGIN;
|
|
||||||
});
|
|
||||||
|
|
||||||
selection
|
|
||||||
.select<SVGTextElement>('text.node_point_label')
|
|
||||||
.text((n: Node) => `(${n.x}, ${n.y})`);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public draw(view: SVGSelection, nodes?: Node[]) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
let nodes_selection: Selection<SVGGElement, Node, any, any> = view
|
|
||||||
.selectAll<SVGGElement, Node>('g.node');
|
|
||||||
|
|
||||||
if (nodes) {
|
|
||||||
nodes_selection = nodes_selection.data(nodes);
|
|
||||||
} else {
|
|
||||||
nodes_selection = nodes_selection.data((l: Layer) => {
|
|
||||||
return l.nodes;
|
|
||||||
}, (n: Node) => {
|
}, (n: Node) => {
|
||||||
return n.node_id;
|
return n.node_id;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const node_enter = nodes_selection
|
const node_enter = node.enter()
|
||||||
.enter()
|
.append<SVGGElement>('g')
|
||||||
.append<SVGGElement>('g')
|
.attr('class', 'node')
|
||||||
.attr('class', 'node');
|
.attr('node_id', (n: Node) => n.node_id)
|
||||||
|
|
||||||
// add image to node
|
const merge = node.merge(node_enter);
|
||||||
node_enter
|
|
||||||
.append<SVGImageElement>('image');
|
|
||||||
|
|
||||||
// add label of node
|
this.nodeWidget.draw(merge);
|
||||||
node_enter
|
|
||||||
.append<SVGTextElement>('text')
|
|
||||||
.attr('class', 'label');
|
|
||||||
|
|
||||||
if (this.debug) {
|
node
|
||||||
node_enter
|
|
||||||
.append<SVGCircleElement>('circle')
|
|
||||||
.attr('class', 'node_point')
|
|
||||||
.attr('r', 2);
|
|
||||||
|
|
||||||
node_enter
|
|
||||||
.append<SVGTextElement>('text')
|
|
||||||
.attr('class', 'node_point_label')
|
|
||||||
.attr('x', '-100')
|
|
||||||
.attr('y', '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
const node_merge = nodes_selection
|
|
||||||
.merge(node_enter)
|
|
||||||
.classed('selected', (n: Node) => n.is_selected)
|
|
||||||
.on("contextmenu", function (n: Node, i: number) {
|
|
||||||
event.preventDefault();
|
|
||||||
self.onContextMenu.emit(new NodeContextMenu(event, n));
|
|
||||||
})
|
|
||||||
.on('click', (n: Node) => {
|
|
||||||
this.onNodeClicked.emit(new NodeClicked(event, n));
|
|
||||||
});
|
|
||||||
|
|
||||||
// update image of node
|
|
||||||
node_merge
|
|
||||||
.select<SVGImageElement>('image')
|
|
||||||
.attr('xlink:href', (n: Node) => {
|
|
||||||
const symbol = this.symbols.find((s: Symbol) => s.symbol_id === n.symbol);
|
|
||||||
if (symbol) {
|
|
||||||
return 'data:image/svg+xml;base64,' + btoa(symbol.raw);
|
|
||||||
}
|
|
||||||
// @todo; we need to have default image
|
|
||||||
return 'data:image/svg+xml;base64,none';
|
|
||||||
})
|
|
||||||
.attr('width', (n: Node) => n.width)
|
|
||||||
.attr('height', (n: Node) => n.height)
|
|
||||||
.attr('x', (n: Node) => 0)
|
|
||||||
.attr('y', (n: Node) => 0)
|
|
||||||
.on('mouseover', function (this, n: Node) {
|
|
||||||
select(this).attr("class", "over");
|
|
||||||
})
|
|
||||||
.on('mouseout', function (this, n: Node) {
|
|
||||||
select(this).attr("class", "");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.revise(node_merge);
|
|
||||||
|
|
||||||
const callback = function (this: SVGGElement, n: Node) {
|
|
||||||
const e: D3DragEvent<SVGGElement, Node, Node> = event;
|
|
||||||
|
|
||||||
n.x = e.x;
|
|
||||||
n.y = e.y;
|
|
||||||
|
|
||||||
self.revise(select(this));
|
|
||||||
self.onNodeDragging.emit(new NodeDragging(event, n));
|
|
||||||
};
|
|
||||||
|
|
||||||
const dragging = () => {
|
|
||||||
return drag<SVGGElement, Node>()
|
|
||||||
.on('drag', callback)
|
|
||||||
.on('end', (n: Node) => {
|
|
||||||
const e: D3DragEvent<SVGGElement, Node, Node> = event;
|
|
||||||
self.onNodeDragged.emit(new NodeDragged(e, n));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.draggingEnabled) {
|
|
||||||
node_merge.call(dragging());
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes_selection
|
|
||||||
.exit()
|
.exit()
|
||||||
.remove();
|
.remove();
|
||||||
|
|
||||||
|
if (this.draggingEnabled) {
|
||||||
|
this.draggable.call(merge);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private selectNode(view: SVGSelection, node: Node) {
|
||||||
|
return view.selectAll<SVGGElement, Node>(`g.node[node_id="${node.node_id}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -34,4 +34,3 @@ export class ApplianceComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
[selection-tool]="tools.selection"
|
[selection-tool]="tools.selection"
|
||||||
[moving-tool]="tools.moving"
|
[moving-tool]="tools.moving"
|
||||||
[draw-link-tool]="tools.draw_link"
|
[draw-link-tool]="tools.draw_link"
|
||||||
(onNodeDragged)="onNodeDragged($event)"
|
[readonly]="inReadOnlyMode"
|
||||||
|
(nodeDragged)="onNodeDragged($event)"
|
||||||
|
(drawingDragged)="onDrawingDragged($event)"
|
||||||
(onLinkCreated)="onLinkCreated($event)"
|
(onLinkCreated)="onLinkCreated($event)"
|
||||||
></app-map>
|
></app-map>
|
||||||
<div class="project-toolbar">
|
<div class="project-toolbar">
|
||||||
|
@ -23,12 +23,14 @@ import { NodesDataSource } from "../../cartography/datasources/nodes-datasource"
|
|||||||
import { LinksDataSource } from "../../cartography/datasources/links-datasource";
|
import { LinksDataSource } from "../../cartography/datasources/links-datasource";
|
||||||
import { ProjectWebServiceHandler } from "../../handlers/project-web-service-handler";
|
import { ProjectWebServiceHandler } from "../../handlers/project-web-service-handler";
|
||||||
import { SelectionManager } from "../../cartography/managers/selection-manager";
|
import { SelectionManager } from "../../cartography/managers/selection-manager";
|
||||||
import { InRectangleHelper } from "../../cartography/helpers/in-rectangle-helper";
|
|
||||||
import { DrawingsDataSource } from "../../cartography/datasources/drawings-datasource";
|
import { DrawingsDataSource } from "../../cartography/datasources/drawings-datasource";
|
||||||
import { ProgressService } from "../../common/progress/progress.service";
|
import { ProgressService } from "../../common/progress/progress.service";
|
||||||
import { MapChangeDetectorRef } from '../../cartography/services/map-change-detector-ref';
|
import { MapChangeDetectorRef } from '../../cartography/services/map-change-detector-ref';
|
||||||
import { NodeContextMenu, NodeDragged } from '../../cartography/events/nodes';
|
import { NodeContextMenu } from '../../cartography/events/nodes';
|
||||||
import { LinkCreated } from '../../cartography/events/links';
|
import { LinkCreated } from '../../cartography/events/links';
|
||||||
|
import { NodeWidget } from '../../cartography/widgets/node';
|
||||||
|
import { DraggedDataEvent } from '../../cartography/events/event-source';
|
||||||
|
import { DrawingService } from '../../services/drawing.service';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -56,33 +58,29 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private inReadOnlyMode = false;
|
private inReadOnlyMode = false;
|
||||||
|
|
||||||
protected selectionManager: SelectionManager;
|
|
||||||
|
|
||||||
@ViewChild(MapComponent) mapChild: MapComponent;
|
@ViewChild(MapComponent) mapChild: MapComponent;
|
||||||
|
|
||||||
@ViewChild(NodeContextMenuComponent) nodeContextMenu: NodeContextMenuComponent;
|
@ViewChild(NodeContextMenuComponent) nodeContextMenu: NodeContextMenuComponent;
|
||||||
|
|
||||||
private subscriptions: Subscription[];
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private serverService: ServerService,
|
private serverService: ServerService,
|
||||||
private projectService: ProjectService,
|
private projectService: ProjectService,
|
||||||
private symbolService: SymbolService,
|
private symbolService: SymbolService,
|
||||||
private nodeService: NodeService,
|
private nodeService: NodeService,
|
||||||
private linkService: LinkService,
|
private linkService: LinkService,
|
||||||
private progressService: ProgressService,
|
private drawingService: DrawingService,
|
||||||
private projectWebServiceHandler: ProjectWebServiceHandler,
|
private progressService: ProgressService,
|
||||||
private mapChangeDetectorRef: MapChangeDetectorRef,
|
private projectWebServiceHandler: ProjectWebServiceHandler,
|
||||||
protected nodesDataSource: NodesDataSource,
|
private mapChangeDetectorRef: MapChangeDetectorRef,
|
||||||
protected linksDataSource: LinksDataSource,
|
private nodeWidget: NodeWidget,
|
||||||
protected drawingsDataSource: DrawingsDataSource,
|
private selectionManager: SelectionManager,
|
||||||
) {
|
protected nodesDataSource: NodesDataSource,
|
||||||
this.selectionManager = new SelectionManager(
|
protected linksDataSource: LinksDataSource,
|
||||||
this.nodesDataSource, this.linksDataSource, this.drawingsDataSource, new InRectangleHelper());
|
protected drawingsDataSource: DrawingsDataSource,
|
||||||
|
) {}
|
||||||
this.subscriptions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.progressService.activate();
|
this.progressService.activate();
|
||||||
@ -188,9 +186,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setUpMapCallbacks(project: Project) {
|
setUpMapCallbacks(project: Project) {
|
||||||
this.mapChild.graphLayout.getNodesWidget().setDraggingEnabled(!this.readonly);
|
const onContextMenu = this.nodeWidget.onContextMenu.subscribe((eventNode: NodeContextMenu) => {
|
||||||
|
|
||||||
const onContextMenu = this.mapChild.graphLayout.getNodesWidget().onContextMenu.subscribe((eventNode: NodeContextMenu) => {
|
|
||||||
this.nodeContextMenu.open(
|
this.nodeContextMenu.open(
|
||||||
eventNode.node,
|
eventNode.node,
|
||||||
eventNode.event.clientY,
|
eventNode.event.clientY,
|
||||||
@ -220,12 +216,21 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onNodeDragged(nodeEvent: NodeDragged) {
|
onNodeDragged(draggedEvent: DraggedDataEvent<Node>) {
|
||||||
this.nodesDataSource.update(nodeEvent.node);
|
this.nodesDataSource.update(draggedEvent.datum);
|
||||||
this.nodeService
|
this.nodeService
|
||||||
.updatePosition(this.server, nodeEvent.node, nodeEvent.node.x, nodeEvent.node.y)
|
.updatePosition(this.server, draggedEvent.datum, draggedEvent.datum.x, draggedEvent.datum.y)
|
||||||
.subscribe((n: Node) => {
|
.subscribe((node: Node) => {
|
||||||
this.nodesDataSource.update(n);
|
this.nodesDataSource.update(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDrawingDragged(draggedEvent: DraggedDataEvent<Drawing>) {
|
||||||
|
this.drawingsDataSource.update(draggedEvent.datum);
|
||||||
|
this.drawingService
|
||||||
|
.updatePosition(this.server, draggedEvent.datum, draggedEvent.datum.x, draggedEvent.datum.y)
|
||||||
|
.subscribe((drawing: Drawing) => {
|
||||||
|
this.drawingsDataSource.update(drawing);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<span>{{confirmationMessage}}</span>
|
||||||
|
<div mat-dialog-actions *ngIf="!isOpen">
|
||||||
|
<button mat-button class="cancelButton" (click)="onNoClick()" color="accent">No</button>
|
||||||
|
<button mat-button class="confirmButton" (click)="onYesClick()" tabindex="2" mat-raised-button color="primary">Yes</button>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions *ngIf="isOpen">
|
||||||
|
<button mat-button (click)="onNoClick()" color="accent">Ok</button>
|
||||||
|
</div>
|
@ -0,0 +1,131 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MatDialogModule, MatDialog } from "@angular/material";
|
||||||
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
import { Component, NgModule } from '@angular/core';
|
||||||
|
import { Project } from '../../../../models/project';
|
||||||
|
import { ImportProjectConfirmationDialogComponent } from './import-project-confirmation-dialog.component';
|
||||||
|
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||||
|
|
||||||
|
describe('ImportProjectConfirmationDialogComponent', () => {
|
||||||
|
let dialog: MatDialog;
|
||||||
|
let overlayContainerElement: HTMLElement;
|
||||||
|
|
||||||
|
let noop: ComponentFixture<NoopComponent>;
|
||||||
|
let existingProject: Project = {
|
||||||
|
auto_close: false,
|
||||||
|
auto_open: false,
|
||||||
|
auto_start: false,
|
||||||
|
filename: "blank",
|
||||||
|
name: "blank",
|
||||||
|
path: "",
|
||||||
|
project_id: "",
|
||||||
|
scene_height: 100,
|
||||||
|
scene_width: 100,
|
||||||
|
status: "",
|
||||||
|
readonly: false,
|
||||||
|
show_interface_labels: false,
|
||||||
|
show_layers: false,
|
||||||
|
show_grid: false,
|
||||||
|
snap_to_grid: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ DialogTestModule ],
|
||||||
|
providers: [
|
||||||
|
{ provide: OverlayContainer, useFactory: () => {
|
||||||
|
overlayContainerElement = document.createElement('div');
|
||||||
|
return { getContainerElement: () => overlayContainerElement };
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog = TestBed.get(MatDialog);
|
||||||
|
|
||||||
|
noop = TestBed.createComponent(NoopComponent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show correct message if project is open', () => {
|
||||||
|
existingProject.status = "opened";
|
||||||
|
const config = {
|
||||||
|
data: {
|
||||||
|
'existingProject' : existingProject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.open(ImportProjectConfirmationDialogComponent, config);
|
||||||
|
noop.detectChanges();
|
||||||
|
|
||||||
|
const message = overlayContainerElement.querySelector('span');
|
||||||
|
expect(message.textContent).toBe("Project blank is open. You can not overwrite it.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show correct message if project is closed', () => {
|
||||||
|
existingProject.status = "closed";
|
||||||
|
const config = {
|
||||||
|
data: {
|
||||||
|
'existingProject' : existingProject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.open(ImportProjectConfirmationDialogComponent, config);
|
||||||
|
noop.detectChanges();
|
||||||
|
|
||||||
|
const message = overlayContainerElement.querySelector('span');
|
||||||
|
expect(message.textContent).toBe("Project blank already exist, overwrite it?");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false after closing when project is open', () => {
|
||||||
|
existingProject.status = "opened";
|
||||||
|
const config = {
|
||||||
|
data: {
|
||||||
|
'existingProject' : existingProject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let dialogRef = dialog.open(ImportProjectConfirmationDialogComponent, config);
|
||||||
|
noop.detectChanges();
|
||||||
|
const button = overlayContainerElement.querySelector('button');
|
||||||
|
spyOn(dialogRef.componentInstance.dialogRef, 'close');
|
||||||
|
button.click();
|
||||||
|
|
||||||
|
expect(dialogRef.componentInstance.dialogRef.close).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true after choosing overriding', () => {
|
||||||
|
existingProject.status = "closed";
|
||||||
|
const config = {
|
||||||
|
data: {
|
||||||
|
'existingProject' : existingProject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let dialogRef = dialog.open(ImportProjectConfirmationDialogComponent, config);
|
||||||
|
noop.detectChanges();
|
||||||
|
const button: HTMLButtonElement = overlayContainerElement.querySelector('.confirmButton');
|
||||||
|
spyOn(dialogRef.componentInstance.dialogRef, 'close');
|
||||||
|
button.click();
|
||||||
|
|
||||||
|
expect(dialogRef.componentInstance.dialogRef.close).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: ''
|
||||||
|
})
|
||||||
|
class NoopComponent {}
|
||||||
|
|
||||||
|
const TEST_DIRECTIVES = [
|
||||||
|
ImportProjectConfirmationDialogComponent,
|
||||||
|
NoopComponent
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [MatDialogModule, NoopAnimationsModule],
|
||||||
|
exports: TEST_DIRECTIVES,
|
||||||
|
declarations: TEST_DIRECTIVES,
|
||||||
|
entryComponents: [
|
||||||
|
ImportProjectConfirmationDialogComponent
|
||||||
|
],
|
||||||
|
})
|
||||||
|
class DialogTestModule { }
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Component, OnInit, Inject } from '@angular/core';
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material";
|
||||||
|
import { Project } from '../../../../models/project';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-import-project-dialog',
|
||||||
|
templateUrl: 'import-project-confirmation-dialog.component.html',
|
||||||
|
styleUrls: ['import-project-confirmation-dialog.component.css']
|
||||||
|
})
|
||||||
|
export class ImportProjectConfirmationDialogComponent implements OnInit {
|
||||||
|
private existingProject : Project;
|
||||||
|
public confirmationMessage : string;
|
||||||
|
public isOpen : boolean;
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<ImportProjectConfirmationDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: any
|
||||||
|
){
|
||||||
|
this.existingProject = data['existingProject']
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(){
|
||||||
|
if(this.existingProject.status === "opened"){
|
||||||
|
this.confirmationMessage = `Project ${this.existingProject.name} is open. You can not overwrite it.`
|
||||||
|
this.isOpen = true;
|
||||||
|
} else {
|
||||||
|
this.confirmationMessage = `Project ${this.existingProject.name} already exist, overwrite it?`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNoClick() : void {
|
||||||
|
this.dialogRef.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onYesClick() : void {
|
||||||
|
this.dialogRef.close(true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
.non-visible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-button{
|
||||||
|
height: 50px;
|
||||||
|
width: 20%;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-form {
|
||||||
|
float: right;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-form-field {
|
||||||
|
margin-left: 5%;
|
||||||
|
width: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: 0;
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-icon {
|
||||||
|
vertical-align: "middle";
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message-box {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background-color : #0097a7;
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<h1 mat-dialog-title>Import project</h1>
|
||||||
|
<div [hidden]="isFirstStepCompleted">
|
||||||
|
<form [formGroup]="projectNameForm" class="file-name-form">
|
||||||
|
<input type="file" accept=".gns3project, .gns3p" class="non-visible" #file (change)="uploadProjectFile($event)" ng2FileSelect [uploader]="uploader"/>
|
||||||
|
<button mat-raised-button color="primary" (click)="file.click()" class="file-button">Choose file</button>
|
||||||
|
<mat-form-field class="file-name-form-field">
|
||||||
|
<input matInput type="text" formControlName="projectName" [ngClass]="{ 'is-invalid': form.projectName.errors }" placeholder="Please enter name" />
|
||||||
|
<mat-error *ngIf="form.projectName.errors && form.projectName.errors.required">Project name is required</mat-error>
|
||||||
|
<mat-error *ngIf="form.projectName.errors && form.projectName.errors.invalidName">Project name is incorrect</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
<button class="delete-button" [hidden]="!isDeleteVisible">
|
||||||
|
<mat-icon color="primary" (click)="onDeleteClick()" class="delete-icon">clear</mat-icon>
|
||||||
|
</button>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button (click)="onNoClick()" color="accent">Cancel</button>
|
||||||
|
<button mat-button [disabled]="!isImportEnabled" (click)="onImportClick()" tabindex="2" mat-raised-button color="primary">Import</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div [hidden]="!isFirstStepCompleted">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': uploader.progress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="result-message-box">
|
||||||
|
<span>{{resultMessage}}</span>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-button [disabled]="!isFinishEnabled" (click)="onNoClick()" tabindex="2" mat-raised-button color="primary">Finish</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,229 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ImportProjectDialogComponent, Validator } from "./import-project-dialog.component";
|
||||||
|
import { Server } from "../../../models/server";
|
||||||
|
import { MatInputModule, MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule, MatStepperModule, MatFormFieldModule, MatDialogRef, MatDialog, MAT_DIALOG_DATA } from "@angular/material";
|
||||||
|
import { RouterTestingModule } from "@angular/router/testing";
|
||||||
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
import { FileUploadModule, FileSelectDirective, FileItem, FileUploader, ParsedResponseHeaders } from "ng2-file-upload";
|
||||||
|
import { FormsModule, ReactiveFormsModule, FormBuilder, FormControl, Validators } from '@angular/forms';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ProjectService } from '../../../services/project.service';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
import { Project } from '../../../models/project';
|
||||||
|
|
||||||
|
export class MockedProjectService {
|
||||||
|
public projects: Project[] = [{
|
||||||
|
auto_close: false,
|
||||||
|
auto_open: false,
|
||||||
|
auto_start: false,
|
||||||
|
filename: "blank",
|
||||||
|
name: "blank",
|
||||||
|
path: "",
|
||||||
|
project_id: "",
|
||||||
|
scene_height: 100,
|
||||||
|
scene_width: 100,
|
||||||
|
status: "opened",
|
||||||
|
readonly: false,
|
||||||
|
show_interface_labels: false,
|
||||||
|
show_layers: false,
|
||||||
|
show_grid: false,
|
||||||
|
snap_to_grid: false,
|
||||||
|
}];
|
||||||
|
|
||||||
|
list(server: Server) {
|
||||||
|
return of(this.projects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ImportProjectDialogComponent', () => {
|
||||||
|
let component: ImportProjectDialogComponent;
|
||||||
|
let fixture: ComponentFixture<ImportProjectDialogComponent>;
|
||||||
|
let server: Server;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
let fileSelectDirective: FileSelectDirective;
|
||||||
|
let formBuilder: FormBuilder;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
MatTableModule,
|
||||||
|
MatTooltipModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatSortModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatStepperModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
NoopAnimationsModule,
|
||||||
|
FileUploadModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: MatDialogRef },
|
||||||
|
{ provide: MAT_DIALOG_DATA },
|
||||||
|
{ provide: ProjectService, useClass: MockedProjectService}
|
||||||
|
],
|
||||||
|
declarations : [ImportProjectDialogComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
server = new Server();
|
||||||
|
server.ip = "localhost";
|
||||||
|
server.port = 80;
|
||||||
|
formBuilder = new FormBuilder();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ImportProjectDialogComponent);
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.server = server;
|
||||||
|
component.projectNameForm = formBuilder.group({
|
||||||
|
projectName: new FormControl(null, [Validators.required, Validator.projectNameValidator])
|
||||||
|
});
|
||||||
|
component.projectNameForm.controls['projectName'].setValue("ValidName");
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
debugElement = fixture.debugElement.query(By.directive(FileSelectDirective));
|
||||||
|
fileSelectDirective = debugElement.injector.get(FileSelectDirective) as FileSelectDirective;
|
||||||
|
component.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(fixture).toBeDefined();
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set file uploader', () => {
|
||||||
|
expect(fileSelectDirective).toBeDefined();
|
||||||
|
expect(fileSelectDirective.uploader).toBe(component.uploader);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle file adding', () => {
|
||||||
|
spyOn(fileSelectDirective.uploader, 'addToQueue');
|
||||||
|
|
||||||
|
fileSelectDirective.onChange();
|
||||||
|
|
||||||
|
const expectedArguments = [ debugElement.nativeElement.files,
|
||||||
|
fileSelectDirective.getOptions(),
|
||||||
|
fileSelectDirective.getFilters() ];
|
||||||
|
expect(fileSelectDirective.uploader.addToQueue).toHaveBeenCalledWith(...expectedArguments);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call uploading item', () => {
|
||||||
|
spyOn(fileSelectDirective.uploader, 'uploadItem');
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(fileSelectDirective.uploader.uploadItem).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call uploading item with correct arguments', () => {
|
||||||
|
let fileItem = new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{});
|
||||||
|
fileSelectDirective.uploader.queue.push(fileItem);
|
||||||
|
spyOn(fileSelectDirective.uploader, 'uploadItem');
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(fileSelectDirective.uploader.uploadItem).toHaveBeenCalledWith(fileItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle file change event', () => {
|
||||||
|
let input = fixture.debugElement.query(By.css('input[type=file]')).nativeElement;
|
||||||
|
spyOn(component, 'uploadProjectFile');
|
||||||
|
|
||||||
|
input.dispatchEvent(new Event('change'));
|
||||||
|
|
||||||
|
expect(component.uploadProjectFile).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear queue after calling delete', () => {
|
||||||
|
fileSelectDirective.uploader.queue.push(new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{}));
|
||||||
|
spyOn(fileSelectDirective.uploader.queue, "pop");
|
||||||
|
|
||||||
|
component.onDeleteClick();
|
||||||
|
|
||||||
|
expect(fileSelectDirective.uploader.queue.pop).toHaveBeenCalled();
|
||||||
|
expect(fileSelectDirective.uploader.queue[0]).toBeNull;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prepare correct upload path for file', () => {
|
||||||
|
fileSelectDirective.uploader.queue.push(new FileItem(fileSelectDirective.uploader,new File([],"fileName"),{}));
|
||||||
|
component.projectNameForm.controls['projectName'].setValue("newProject");
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(fileSelectDirective.uploader.queue[0].url).toContain("localhost:80");
|
||||||
|
expect(fileSelectDirective.uploader.queue[0].url).toContain("newProject");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to progress view after clicking import', () => {
|
||||||
|
let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{});
|
||||||
|
fileSelectDirective.uploader.queue.push(fileItem);
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(component.isFirstStepCompleted).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect if file input is empty', () => {
|
||||||
|
component.projectNameForm.controls['projectName'].setValue("");
|
||||||
|
fixture.detectChanges();
|
||||||
|
spyOn(fileSelectDirective.uploader, 'uploadItem');
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(fileSelectDirective.uploader.uploadItem).not.toHaveBeenCalled();
|
||||||
|
expect(component.projectNameForm.valid).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sanitize file name input', () => {
|
||||||
|
component.projectNameForm.controls['projectName'].setValue("[][]");
|
||||||
|
fixture.detectChanges();
|
||||||
|
spyOn(fileSelectDirective.uploader, 'uploadItem');
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(fileSelectDirective.uploader.uploadItem).not.toHaveBeenCalled();
|
||||||
|
expect(component.projectNameForm.valid).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open confirmation dialog if project with the same exists', () => {
|
||||||
|
component.projectNameForm.controls['projectName'].setValue("blank");
|
||||||
|
spyOn(component, "openConfirmationDialog");
|
||||||
|
|
||||||
|
component.onImportClick();
|
||||||
|
|
||||||
|
expect(component.openConfirmationDialog).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show delete button after selecting project', () => {
|
||||||
|
let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{});
|
||||||
|
fileSelectDirective.uploader.queue.push(fileItem);
|
||||||
|
let event = {
|
||||||
|
target: {
|
||||||
|
files: [ {name : "uploadedFile"} ]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
component.uploadProjectFile(event);
|
||||||
|
|
||||||
|
expect(component.isDeleteVisible).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide delete button after deselecting project', () => {
|
||||||
|
let fileItem = new FileItem(fileSelectDirective.uploader, new File([],"fileName"),{});
|
||||||
|
fileSelectDirective.uploader.queue.push(fileItem);
|
||||||
|
let event = {
|
||||||
|
target: {
|
||||||
|
files: [ {name : "uploadedFile"} ]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
component.uploadProjectFile(event);
|
||||||
|
component.onDeleteClick();
|
||||||
|
|
||||||
|
expect(component.isDeleteVisible).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,146 @@
|
|||||||
|
import { Component, OnInit, Inject, ViewChild } from '@angular/core';
|
||||||
|
import { MatStepper, MatDialogRef, MAT_DIALOG_DATA, MatDialog } from "@angular/material";
|
||||||
|
import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload';
|
||||||
|
import { Server } from '../../../models/server';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||||
|
import { ProjectService } from '../../../services/project.service';
|
||||||
|
import { Project } from '../../../models/project';
|
||||||
|
import { ImportProjectConfirmationDialogComponent } from './import-project-confirmation-dialog/import-project-confirmation-dialog.component';
|
||||||
|
import { ServerResponse } from '../../../models/serverResponse';
|
||||||
|
|
||||||
|
export class Validator {
|
||||||
|
static projectNameValidator(projectName) {
|
||||||
|
var pattern = new RegExp(/[~`!#$%\^&*+=\[\]\\';,/{}|\\":<>\?]/);
|
||||||
|
|
||||||
|
if(!pattern.test(projectName.value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { invalidName: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-import-project-dialog',
|
||||||
|
templateUrl: 'import-project-dialog.component.html',
|
||||||
|
styleUrls: ['import-project-dialog.component.css']
|
||||||
|
})
|
||||||
|
export class ImportProjectDialogComponent implements OnInit {
|
||||||
|
uploader: FileUploader;
|
||||||
|
server : Server;
|
||||||
|
isImportEnabled : boolean = false;
|
||||||
|
isFinishEnabled : boolean = false;
|
||||||
|
isDeleteVisible : boolean = false;
|
||||||
|
resultMessage : string = "The project is being imported... Please wait";
|
||||||
|
projectNameForm: FormGroup;
|
||||||
|
submitted: boolean = false;
|
||||||
|
isFirstStepCompleted: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dialog: MatDialog,
|
||||||
|
public dialogRef: MatDialogRef<ImportProjectDialogComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private projectService: ProjectService){
|
||||||
|
this.projectNameForm = this.formBuilder.group({
|
||||||
|
projectName: new FormControl(null, [Validators.required, Validator.projectNameValidator])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(){
|
||||||
|
this.uploader = new FileUploader({});
|
||||||
|
this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; };
|
||||||
|
|
||||||
|
this.uploader.onErrorItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {
|
||||||
|
let serverResponse : ServerResponse = JSON.parse(response);
|
||||||
|
this.resultMessage = "An error occured: " + serverResponse.message;
|
||||||
|
this.isFinishEnabled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.uploader.onCompleteItem = (item: FileItem, response: string, status: number, headers: ParsedResponseHeaders) => {
|
||||||
|
this.resultMessage = "Project was imported succesfully!";
|
||||||
|
this.isFinishEnabled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get form() {
|
||||||
|
return this.projectNameForm.controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadProjectFile(event) : void{
|
||||||
|
this.projectNameForm.controls['projectName'].setValue(event.target.files[0].name.split(".")[0]);
|
||||||
|
this.isImportEnabled = true;
|
||||||
|
this.isDeleteVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onImportClick() : void{
|
||||||
|
if (this.projectNameForm.invalid){
|
||||||
|
this.submitted = true;
|
||||||
|
} else {
|
||||||
|
this.projectService
|
||||||
|
.list(this.server)
|
||||||
|
.subscribe((projects: Project[]) => {
|
||||||
|
const projectName = this.projectNameForm.controls['projectName'].value;
|
||||||
|
let existingProject = projects.find(project => project.name === projectName);
|
||||||
|
|
||||||
|
if (existingProject){
|
||||||
|
this.openConfirmationDialog(existingProject);
|
||||||
|
} else {
|
||||||
|
this.importProject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importProject(){
|
||||||
|
const url = this.prepareUploadPath();
|
||||||
|
this.uploader.queue.forEach(elem => elem.url = url);
|
||||||
|
|
||||||
|
this.isFirstStepCompleted = true;
|
||||||
|
|
||||||
|
const itemToUpload = this.uploader.queue[0];
|
||||||
|
this.uploader.uploadItem(itemToUpload);
|
||||||
|
}
|
||||||
|
|
||||||
|
openConfirmationDialog(existingProject: Project) {
|
||||||
|
const dialogRef = this.dialog.open(ImportProjectConfirmationDialogComponent, {
|
||||||
|
width: '300px',
|
||||||
|
height: '150px',
|
||||||
|
data: {
|
||||||
|
'existingProject': existingProject
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((answer: boolean) => {
|
||||||
|
if (answer) {
|
||||||
|
this.projectService.close(this.server, existingProject.project_id).subscribe(() => {
|
||||||
|
this.projectService.delete(this.server, existingProject.project_id).subscribe(() => {
|
||||||
|
this.importProject();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onNoClick() : void{
|
||||||
|
this.uploader.cancelAll();
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFinishClick() : void{
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteClick() : void{
|
||||||
|
this.uploader.queue.pop();
|
||||||
|
this.isImportEnabled = false;
|
||||||
|
this.isDeleteVisible = false;
|
||||||
|
this.projectNameForm.controls['projectName'].setValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareUploadPath() : string{
|
||||||
|
const projectName = this.projectNameForm.controls['projectName'].value;
|
||||||
|
return `http://${this.server.ip}:${this.server.port}/v2/projects/${uuid()}/import?name=${projectName}`;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
.import-button {
|
||||||
|
height: 40px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="default-header">
|
<div class="default-header">
|
||||||
<h1>Projects</h1>
|
<div class="row">
|
||||||
|
<h1 class="col">Projects</h1>
|
||||||
|
<button class="col" mat-raised-button color="primary" (click)="importProject()" class="import-button">Import project</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="default-content">
|
<div class="default-content">
|
||||||
|
|
||||||
@ -33,6 +36,5 @@
|
|||||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { MatIconModule, MatSortModule, MatTableModule, MatTooltipModule } from "@angular/material";
|
import { MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule } from "@angular/material";
|
||||||
import { RouterTestingModule } from "@angular/router/testing";
|
import { RouterTestingModule } from "@angular/router/testing";
|
||||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ describe('ProjectsComponent', () => {
|
|||||||
MatTooltipModule,
|
MatTooltipModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
|
MatDialogModule,
|
||||||
NoopAnimationsModule,
|
NoopAnimationsModule,
|
||||||
RouterTestingModule.withRoutes([]),
|
RouterTestingModule.withRoutes([]),
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { MatSort, MatSortable } from "@angular/material";
|
import { MatSort, MatSortable, MatDialog } from "@angular/material";
|
||||||
|
|
||||||
import { DataSource } from "@angular/cdk/collections";
|
import { DataSource } from "@angular/cdk/collections";
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ import { ServerService } from "../../services/server.service";
|
|||||||
import { SettingsService, Settings } from "../../services/settings.service";
|
import { SettingsService, Settings } from "../../services/settings.service";
|
||||||
import { ProgressService } from "../../common/progress/progress.service";
|
import { ProgressService } from "../../common/progress/progress.service";
|
||||||
|
|
||||||
|
import { ImportProjectDialogComponent } from './import-project-dialog/import-project-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-projects',
|
selector: 'app-projects',
|
||||||
@ -33,7 +34,8 @@ export class ProjectsComponent implements OnInit {
|
|||||||
private serverService: ServerService,
|
private serverService: ServerService,
|
||||||
private projectService: ProjectService,
|
private projectService: ProjectService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
private progressService: ProgressService
|
private progressService: ProgressService,
|
||||||
|
private dialog: MatDialog
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -96,6 +98,18 @@ export class ProjectsComponent implements OnInit {
|
|||||||
this.progressService.deactivate();
|
this.progressService.deactivate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
importProject(){
|
||||||
|
const dialogRef = this.dialog.open(ImportProjectDialogComponent, {
|
||||||
|
width: '550px',
|
||||||
|
});
|
||||||
|
let instance = dialogRef.componentInstance;
|
||||||
|
instance.server = this.server;
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(() => {
|
||||||
|
this.refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4
src/app/models/serverResponse.ts
Normal file
4
src/app/models/serverResponse.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export class ServerResponse {
|
||||||
|
message: string;
|
||||||
|
status: number;
|
||||||
|
}
|
@ -41,7 +41,7 @@ describe('ApplianceService', () => {
|
|||||||
server.port = 3080;
|
server.port = 3080;
|
||||||
server.authorization = "none";
|
server.authorization = "none";
|
||||||
|
|
||||||
service.list(server).subscribe();
|
service.list(server).subscribe(() => {});
|
||||||
|
|
||||||
httpTestingController.expectOne('http://127.0.0.1:3080/v2/appliances');
|
httpTestingController.expectOne('http://127.0.0.1:3080/v2/appliances');
|
||||||
|
|
||||||
|
95
src/app/services/drawing.service.spec.ts
Normal file
95
src/app/services/drawing.service.spec.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
|
import { HttpServer } from './http-server.service';
|
||||||
|
import { Server } from '../models/server';
|
||||||
|
import { Drawing } from '../cartography/models/drawing';
|
||||||
|
import { getTestServer } from './testing';
|
||||||
|
import { DrawingService } from './drawing.service';
|
||||||
|
import { AppTestingModule } from "../testing/app-testing/app-testing.module";
|
||||||
|
|
||||||
|
|
||||||
|
describe('DrawingService', () => {
|
||||||
|
let httpClient: HttpClient;
|
||||||
|
let httpTestingController: HttpTestingController;
|
||||||
|
let httpServer: HttpServer;
|
||||||
|
let service: DrawingService;
|
||||||
|
let server: Server;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
AppTestingModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
HttpServer,
|
||||||
|
DrawingService
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
httpClient = TestBed.get(HttpClient);
|
||||||
|
httpTestingController = TestBed.get(HttpTestingController);
|
||||||
|
httpServer = TestBed.get(HttpServer);
|
||||||
|
service = TestBed.get(DrawingService);
|
||||||
|
server = getTestServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpTestingController.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', inject([DrawingService], (service: DrawingService) => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should updatePosition of drawing', inject([DrawingService], (service: DrawingService) => {
|
||||||
|
const drawing = new Drawing();
|
||||||
|
drawing.project_id = "myproject";
|
||||||
|
drawing.drawing_id = "id";
|
||||||
|
|
||||||
|
service.updatePosition(server, drawing, 10, 20).subscribe();
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
'http://127.0.0.1:3080/v2/projects/myproject/drawings/id');
|
||||||
|
expect(req.request.method).toEqual("PUT");
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
'x': 10,
|
||||||
|
'y': 20
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should update drawing', inject([DrawingService], (service: DrawingService) => {
|
||||||
|
const drawing = new Drawing();
|
||||||
|
drawing.project_id = "myproject";
|
||||||
|
drawing.drawing_id = "id";
|
||||||
|
drawing.x = 10;
|
||||||
|
drawing.y = 20;
|
||||||
|
drawing.z = 30;
|
||||||
|
|
||||||
|
service.update(server, drawing).subscribe();
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
'http://127.0.0.1:3080/v2/projects/myproject/drawings/id');
|
||||||
|
expect(req.request.method).toEqual("PUT");
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
'x': 10,
|
||||||
|
'y': 20,
|
||||||
|
'z': 30
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should delete drawing', inject([DrawingService], (service: DrawingService) => {
|
||||||
|
const drawing = new Drawing();
|
||||||
|
drawing.project_id = "myproject";
|
||||||
|
drawing.drawing_id = "id";
|
||||||
|
|
||||||
|
service.delete(server, drawing).subscribe();
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
'http://127.0.0.1:3080/v2/projects/myproject/drawings/id');
|
||||||
|
expect(req.request.method).toEqual("DELETE");
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
36
src/app/services/drawing.service.ts
Normal file
36
src/app/services/drawing.service.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Drawing } from '../cartography/models/drawing';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import 'rxjs/add/operator/map';
|
||||||
|
import { Server } from "../models/server";
|
||||||
|
import { HttpServer } from "./http-server.service";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DrawingService {
|
||||||
|
|
||||||
|
constructor(private httpServer: HttpServer) { }
|
||||||
|
|
||||||
|
updatePosition(server: Server, drawing: Drawing, x: number, y: number): Observable<Drawing> {
|
||||||
|
return this.httpServer
|
||||||
|
.put<Drawing>(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`, {
|
||||||
|
'x': x,
|
||||||
|
'y': y
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(server: Server, drawing: Drawing): Observable<Drawing> {
|
||||||
|
return this.httpServer
|
||||||
|
.put<Drawing>(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`, {
|
||||||
|
'x': drawing.x,
|
||||||
|
'y': drawing.y,
|
||||||
|
'z': drawing.z
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(server: Server, drawing: Drawing) {
|
||||||
|
return this.httpServer.delete<Drawing>(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { TestBed, inject } from '@angular/core/testing';
|
import { TestBed, inject, fakeAsync } from '@angular/core/testing';
|
||||||
import { PersistenceService, StorageType } from "angular-persistence";
|
import { PersistenceService, StorageType } from "angular-persistence";
|
||||||
|
|
||||||
import { Settings, SettingsService } from './settings.service';
|
import { Settings, SettingsService } from './settings.service';
|
||||||
@ -23,7 +23,7 @@ describe('SettingsService', () => {
|
|||||||
persistenceService = TestBed.get(PersistenceService);
|
persistenceService = TestBed.get(PersistenceService);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
beforeEach(() => {
|
||||||
persistenceService.removeAll(StorageType.LOCAL);
|
persistenceService.removeAll(StorageType.LOCAL);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ describe('SettingsService', () => {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should execute subscriber', inject([SettingsService], (service: SettingsService) => {
|
it('should execute subscriber', inject([SettingsService], fakeAsync((service: SettingsService) => {
|
||||||
let changedSettings: Settings;
|
let changedSettings: Settings;
|
||||||
|
|
||||||
service.set('crash_reports', true);
|
service.set('crash_reports', true);
|
||||||
@ -79,7 +79,7 @@ describe('SettingsService', () => {
|
|||||||
service.set('crash_reports', false);
|
service.set('crash_reports', false);
|
||||||
|
|
||||||
expect(changedSettings.crash_reports).toEqual(false);
|
expect(changedSettings.crash_reports).toEqual(false);
|
||||||
}));
|
})));
|
||||||
|
|
||||||
it('should get isExperimentalEnabled when turned on', inject([SettingsService], (service: SettingsService) => {
|
it('should get isExperimentalEnabled when turned on', inject([SettingsService], (service: SettingsService) => {
|
||||||
service.set('experimental_features', true);
|
service.set('experimental_features', true);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user