Merge branch 'master' into greenkeeper/electron-2.0.6

This commit is contained in:
ziajka 2018-11-15 11:59:20 +01:00 committed by GitHub
commit bdc80d3f68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 3031 additions and 1260 deletions

View File

@ -151,7 +151,7 @@
}
},
"gns3-web-ui-e2e": {
"root": "",
"root": "e2e",
"sourceRoot": "e2e",
"projectType": "application",
"architect": {

View File

@ -37,7 +37,7 @@
"@angular/platform-browser": "^6.0.7",
"@angular/platform-browser-dynamic": "^6.0.7",
"@angular/router": "^6.0.7",
"@ng-bootstrap/ng-bootstrap": "^2.2.0",
"@ng-bootstrap/ng-bootstrap": "^3.0.0",
"angular-persistence": "^1.0.1",
"angular2-hotkeys": "^2.1.2",
"angular2-indexeddb": "^1.2.2",
@ -64,15 +64,15 @@
"@angular/language-service": "^6.0.7",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~10.5.2",
"@types/node": "~10.7.0",
"@sentry/electron": "^0.7.0",
"codelyzer": "~4.4.2",
"electron": "2.0.6",
"electron-builder": "^20.19.2",
"jasmine-core": "~3.1.0",
"jasmine-core": "~3.2.0",
"jasmine-spec-reporter": "~4.2.1",
"jquery": "^3.3.1",
"karma": "~2.0.4",
"karma": "~3.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "^2.0.1",

View File

@ -1,6 +1,6 @@
setuptools==38.4
cx_Freeze==5.1.1
requests==2.18.4
requests==2.20.0
packaging==16.8
appdirs==1.4.3
psutil==5.4.0

View File

@ -6,26 +6,6 @@ import { CdkTableModule } from "@angular/cdk/table";
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
MatButtonModule,
MatCardModule,
MatMenuModule,
MatToolbarModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatTableModule,
MatDialogModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatCheckboxModule,
MatListModule,
MatExpansionModule,
MatSortModule,
MatSelectModule,
MatTooltipModule
} from '@angular/material';
import { D3Service } from 'd3-ng2-service';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
@ -52,14 +32,13 @@ import { DefaultLayoutComponent } from './layouts/default-layout/default-layout.
import { ProgressDialogComponent } from './common/progress-dialog/progress-dialog.component';
import { AppComponent } from './app.component';
import { CreateSnapshotDialogComponent, ProjectMapComponent } from './components/project-map/project-map.component';
import { ProjectMapComponent } from './components/project-map/project-map.component';
import { ServersComponent, AddServerDialogComponent } from './components/servers/servers.component';
import { NodeContextMenuComponent } from './components/project-map/node-context-menu/node-context-menu.component';
import { StartNodeActionComponent } from './components/project-map/node-context-menu/actions/start-node-action/start-node-action.component';
import { StopNodeActionComponent } from './components/project-map/node-context-menu/actions/stop-node-action/stop-node-action.component';
import { ApplianceComponent } from './components/appliance/appliance.component';
import { ApplianceListDialogComponent } from './components/appliance/appliance-list-dialog/appliance-list-dialog.component';
import { NodeSelectInterfaceComponent } from './components/project-map/node-select-interface/node-select-interface.component';
import { CartographyModule } from './cartography/cartography.module';
import { ToasterService } from './services/toaster.service';
import { ProjectWebServiceHandler } from "./handlers/project-web-service-handler";
@ -67,7 +46,7 @@ import { LinksDataSource } from "./cartography/datasources/links-datasource";
import { NodesDataSource } from "./cartography/datasources/nodes-datasource";
import { SymbolsDataSource } from "./cartography/datasources/symbols-datasource";
import { SelectionManager } from "./cartography/managers/selection-manager";
import { InRectangleHelper } from "./cartography/components/map/helpers/in-rectangle-helper";
import { InRectangleHelper } from "./cartography/helpers/in-rectangle-helper";
import { DrawingsDataSource } from "./cartography/datasources/drawings-datasource";
import { MoveLayerDownActionComponent } from './components/project-map/node-context-menu/actions/move-layer-down-action/move-layer-down-action.component';
import { MoveLayerUpActionComponent } from './components/project-map/node-context-menu/actions/move-layer-up-action/move-layer-up-action.component';
@ -82,6 +61,12 @@ import { version } from "./version";
import { ToasterErrorHandler } from "./common/error-handlers/toaster-error-handler";
import { environment } from "../environments/environment";
import { RavenState } from "./common/error-handlers/raven-state-communicator";
import { ServerDiscoveryComponent } from "./components/servers/server-discovery/server-discovery.component";
import { ServerDatabase } from './services/server.database';
import { CreateSnapshotDialogComponent } from './components/snapshots/create-snapshot-dialog/create-snapshot-dialog.component';
import { SnapshotsComponent } from './components/snapshots/snapshots.component';
import { SnapshotMenuItemComponent } from './components/snapshots/snapshot-menu-item/snapshot-menu-item.component';
import { MATERIAL_IMPORTS } from './material.imports';
if (environment.production) {
@ -103,6 +88,8 @@ if (environment.production) {
ServersComponent,
AddServerDialogComponent,
CreateSnapshotDialogComponent,
SnapshotMenuItemComponent,
SnapshotsComponent,
ProjectsComponent,
DefaultLayoutComponent,
ProgressDialogComponent,
@ -111,13 +98,13 @@ if (environment.production) {
StopNodeActionComponent,
ApplianceComponent,
ApplianceListDialogComponent,
NodeSelectInterfaceComponent,
MoveLayerDownActionComponent,
MoveLayerUpActionComponent,
ProjectMapShortcutsComponent,
SettingsComponent,
LocalServerComponent,
ProgressComponent,
ServerDiscoveryComponent,
],
imports: [
NgbModule.forRoot(),
@ -127,28 +114,11 @@ if (environment.production) {
FormsModule,
BrowserAnimationsModule,
CdkTableModule,
MatButtonModule,
MatMenuModule,
MatCardModule,
MatToolbarModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatTableModule,
MatDialogModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatCheckboxModule,
MatListModule,
MatExpansionModule,
MatSortModule,
MatSelectModule,
MatTooltipModule,
CartographyModule,
HotkeyModule.forRoot(),
PersistenceModule,
NgxElectronModule
NgxElectronModule,
...MATERIAL_IMPORTS
],
providers: [
SettingsService,
@ -174,7 +144,8 @@ if (environment.production) {
SelectionManager,
InRectangleHelper,
DrawingsDataSource,
ServerErrorHandler
ServerErrorHandler,
ServerDatabase
],
entryComponents: [
AddServerDialogComponent,

View File

@ -1,12 +1,44 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatMenuModule, MatIconModule } from '@angular/material';
import { MapComponent } from './components/map/map.component';
import { DrawLinkToolComponent } from './components/draw-link-tool/draw-link-tool.component';
import { NodeSelectInterfaceComponent } from './components/node-select-interface/node-select-interface.component';
import { CssFixer } from './helpers/css-fixer';
import { FontFixer } from './helpers/font-fixer';
import { MultiLinkCalculatorHelper } from './helpers/multi-link-calculator-helper';
import { SvgToDrawingConverter } from './helpers/svg-to-drawing-converter';
import { QtDasharrayFixer } from './helpers/qt-dasharray-fixer';
import { LayersManager } from './managers/layers-manager';
import { MapChangeDetectorRef } from './services/map-change-detector-ref';
import { Context } from './models/context';
import { D3_MAP_IMPORTS } from './d3-map.imports';
@NgModule({
imports: [
CommonModule
CommonModule,
MatMenuModule,
MatIconModule
],
declarations: [MapComponent],
exports: [MapComponent]
declarations: [
MapComponent,
DrawLinkToolComponent,
NodeSelectInterfaceComponent
],
providers: [
CssFixer,
FontFixer,
MultiLinkCalculatorHelper,
SvgToDrawingConverter,
QtDasharrayFixer,
LayersManager,
MapChangeDetectorRef,
Context,
...D3_MAP_IMPORTS
],
exports: [ MapComponent ]
})
export class CartographyModule { }

View File

@ -0,0 +1 @@
<app-node-select-interface (onChooseInterface)="onChooseInterface($event)"></app-node-select-interface>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DrawLinkToolComponent } from './draw-link-tool.component';
describe('DrawLinkToolComponent', () => {
let component: DrawLinkToolComponent;
let fixture: ComponentFixture<DrawLinkToolComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DrawLinkToolComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DrawLinkToolComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -0,0 +1,59 @@
import { Component, OnInit, Output, EventEmitter, OnDestroy, ViewChild } from '@angular/core';
import { Port } from '../../../models/port';
import { DrawingLineWidget } from '../../widgets/drawing-line';
import { Node } from '../../models/node';
import { NodesWidget } from '../../widgets/nodes';
import { Subscription } from 'rxjs';
import { NodeSelectInterfaceComponent } from '../node-select-interface/node-select-interface.component';
import { LinkCreated } from '../../events/links';
import { NodeClicked } from '../../events/nodes';
@Component({
selector: 'app-draw-link-tool',
templateUrl: './draw-link-tool.component.html',
styleUrls: ['./draw-link-tool.component.scss']
})
export class DrawLinkToolComponent implements OnInit, OnDestroy {
@ViewChild(NodeSelectInterfaceComponent) nodeSelectInterfaceMenu: NodeSelectInterfaceComponent;
@Output('linkCreated') linkCreated = new EventEmitter<LinkCreated>();
private onNodeClicked: Subscription;
constructor(
private drawingLineTool: DrawingLineWidget,
private nodesWidget: NodesWidget
) { }
ngOnInit() {
this.onNodeClicked = this.nodesWidget.onNodeClicked.subscribe((eventNode: NodeClicked) => {
this.nodeSelectInterfaceMenu.open(
eventNode.node,
eventNode.event.clientY,
eventNode.event.clientX
);
});
}
ngOnDestroy() {
if(this.drawingLineTool.isDrawing()) {
this.drawingLineTool.stop();
}
this.onNodeClicked.unsubscribe();
}
public onChooseInterface(event) {
const node: Node = event.node;
const port: Port = event.port;
if (this.drawingLineTool.isDrawing()) {
const data = this.drawingLineTool.stop();
this.linkCreated.emit(new LinkCreated(data['node'], data['port'], node, port));
} else {
this.drawingLineTool.start(node.x + node.width / 2., node.y + node.height / 2., {
'node': node,
'port': port
});
}
}
}

View File

@ -1,2 +1,10 @@
<svg class="map" preserveAspectRatio="none">
<svg
class="map"
preserveAspectRatio="none"
>
<filter id="grayscale">
<feColorMatrix id="feGrayscale" type="saturate" values="0"/>
</filter>
</svg>
<app-draw-link-tool *ngIf="drawLinkTool" (linkCreated)="linkCreated($event)"></app-draw-link-tool>

Before

Width:  |  Height:  |  Size: 52 B

After

Width:  |  Height:  |  Size: 260 B

View File

@ -1,16 +1,25 @@
import {
Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChange
Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChange, EventEmitter, Output
} from '@angular/core';
import { D3, D3Service } from 'd3-ng2-service';
import { select, Selection } from 'd3-selection';
import { Selection } from 'd3-selection';
import { Node } from "../../models/node";
import { Link } from "../../models/link";
import { Link } from "../../../models/link";
import { GraphLayout } from "../../widgets/graph-layout";
import { Context } from "../../models/context";
import { Size } from "../../models/size";
import { Drawing } from "../../models/drawing";
import { Symbol } from "../../models/symbol";
import { Symbol } from '../../../models/symbol';
import { NodesWidget } from '../../widgets/nodes';
import { Subscription } from 'rxjs';
import { InterfaceLabelWidget } from '../../widgets/interface-label';
import { SelectionTool } from '../../tools/selection-tool';
import { MovingTool } from '../../tools/moving-tool';
import { LinksWidget } from '../../widgets/links';
import { MapChangeDetectorRef } from '../../services/map-change-detector-ref';
import { NodeDragging, NodeDragged } from '../../events/nodes';
import { LinkCreated } from '../../events/links';
@Component({
@ -27,20 +36,60 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
@Input() width = 1500;
@Input() height = 600;
@Output() onNodeDragged: EventEmitter<NodeDragged>;
@Output() onLinkCreated = new EventEmitter<LinkCreated>();
private d3: D3;
private parentNativeElement: any;
private svg: Selection<SVGSVGElement, any, null, undefined>;
private graphContext: Context;
public graphLayout: GraphLayout;
private isReady = false;
constructor(protected element: ElementRef,
protected d3Service: D3Service
) {
private onNodeDraggingSubscription: Subscription;
private onChangesDetected: Subscription;
protected settings = {
'show_interface_labels': true
};
constructor(
private context: Context,
private mapChangeDetectorRef: MapChangeDetectorRef,
protected element: ElementRef,
protected d3Service: D3Service,
protected nodesWidget: NodesWidget,
protected linksWidget: LinksWidget,
protected interfaceLabelWidget: InterfaceLabelWidget,
protected selectionToolWidget: SelectionTool,
protected movingToolWidget: MovingTool,
public graphLayout: GraphLayout
) {
this.d3 = d3Service.getD3();
this.parentNativeElement = element.nativeElement;
this.onNodeDragged = nodesWidget.onNodeDragged;
}
@Input('show-interface-labels')
set showInterfaceLabels(value) {
this.settings.show_interface_labels = value;
this.interfaceLabelWidget.setEnabled(value);
this.mapChangeDetectorRef.detectChanges();
}
@Input('moving-tool')
set movingTool(value) {
this.movingToolWidget.setEnabled(value);
this.mapChangeDetectorRef.detectChanges();
}
@Input('selection-tool')
set selectionTool(value) {
this.selectionToolWidget.setEnabled(value);
this.mapChangeDetectorRef.detectChanges();
}
@Input('draw-link-tool') drawLinkTool: boolean;
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
if (
(changes['width'] && !changes['width'].isFirstChange()) ||
@ -67,37 +116,37 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
ngOnDestroy() {
this.graphLayout.disconnect(this.svg);
this.onNodeDraggingSubscription.unsubscribe();
this.onChangesDetected.unsubscribe();
}
ngOnInit() {
const d3 = this.d3;
let rootElement: Selection<HTMLElement, any, null, undefined>;
if (this.parentNativeElement !== null) {
rootElement = d3.select(this.parentNativeElement);
this.svg = rootElement.select<SVGSVGElement>('svg');
this.graphContext = new Context(true);
this.graphContext.size = this.getSize();
this.graphLayout = new GraphLayout();
this.graphLayout.connect(this.svg, this.graphContext);
this.graphLayout.getNodesWidget().addOnNodeDraggingCallback((event: any, n: Node) => {
const linksWidget = this.graphLayout.getLinksWidget();
linksWidget.select(this.svg).each(function(this: SVGGElement, link: Link) {
if (link.target.node_id === n.node_id || link.source.node_id === n.node_id) {
const selection = select<SVGElement, Link>(this);
linksWidget.revise(selection);
}
});
});
this.graphLayout.draw(this.svg, this.graphContext);
this.createGraph(this.parentNativeElement);
}
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(() => {
if (this.isReady) {
this.reload();
}
});
}
public createGraph(domElement: HTMLElement) {
const rootElement = this.d3.select(domElement);
this.svg = rootElement.select<SVGSVGElement>('svg');
this.graphLayout.connect(this.svg, this.context);
this.graphLayout.draw(this.svg, this.context);
this.isReady = true;
}
public getSize(): Size {
@ -112,9 +161,13 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
return new Size(width, height);
}
protected linkCreated(evt) {
this.onLinkCreated.emit(evt);
}
private changeLayout() {
if (this.parentNativeElement != null) {
this.graphContext.size = this.getSize();
this.context.size = this.getSize();
}
this.graphLayout.setNodes(this.nodes);
@ -156,7 +209,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
}
public redraw() {
this.graphLayout.draw(this.svg, this.graphContext);
this.graphLayout.draw(this.svg, this.context);
}
public reload() {

View File

@ -1,6 +1,6 @@
<div class="context-menu" [style.left]="leftPosition" [style.top]="topPosition" *ngIf="node">
<span [matMenuTriggerFor]="selectInterfaceMenu"></span>
<mat-menu #selectInterfaceMenu="matMenu">
<mat-menu #selectInterfaceMenu="matMenu" class="context-menu-items">
<button mat-menu-item *ngFor="let port of node.ports" (click)="chooseInterface(port)">
<mat-icon>add_circle_outline</mat-icon>
<span>{{ port.name }}</span>

View File

@ -1,8 +1,8 @@
import {ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
import {MatMenuTrigger} from "@angular/material";
import {DomSanitizer} from "@angular/platform-browser";
import {Node} from "../../../cartography/models/node";
import {Port} from "../../../models/port";
import { ChangeDetectorRef, Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
import { MatMenuTrigger } from "@angular/material";
import { DomSanitizer } from "@angular/platform-browser";
import { Node } from "../../../cartography/models/node";
import { Port } from "../../../models/port";
@Component({
@ -15,13 +15,15 @@ export class NodeSelectInterfaceComponent implements OnInit {
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
private topPosition;
private leftPosition;
protected topPosition;
protected leftPosition;
public node: Node;
constructor(
private sanitizer: DomSanitizer,
private changeDetector: ChangeDetectorRef) {}
private changeDetector: ChangeDetectorRef,
) {}
ngOnInit() {
this.setPosition(0, 0);

View File

@ -0,0 +1,35 @@
import { GraphLayout } from './widgets/graph-layout';
import { LinksWidget } from './widgets/links';
import { NodesWidget } from './widgets/nodes';
import { DrawingsWidget } from './widgets/drawings';
import { DrawingLineWidget } from './widgets/drawing-line';
import { SelectionTool } from './tools/selection-tool';
import { MovingTool } from './tools/moving-tool';
import { LayersWidget } from './widgets/layers';
import { LinkWidget } from './widgets/link';
import { InterfaceStatusWidget } from './widgets/interface-status';
import { InterfaceLabelWidget } from './widgets/interface-label';
import { EllipseDrawingWidget } from './widgets/drawings/ellipse-drawing';
import { ImageDrawingWidget } from './widgets/drawings/image-drawing';
import { RectDrawingWidget } from './widgets/drawings/rect-drawing';
import { TextDrawingWidget } from './widgets/drawings/text-drawing';
import { LineDrawingWidget } from './widgets/drawings/line-drawing';
export const D3_MAP_IMPORTS = [
GraphLayout,
LinksWidget,
NodesWidget,
DrawingsWidget,
DrawingLineWidget,
SelectionTool,
MovingTool,
LayersWidget,
LinkWidget,
InterfaceStatusWidget,
InterfaceLabelWidget,
EllipseDrawingWidget,
ImageDrawingWidget,
LineDrawingWidget,
RectDrawingWidget,
TextDrawingWidget,
];

View File

@ -9,7 +9,7 @@ class TestDataSource extends DataSource<Item> {
protected findIndex(item: Item) {
return this.data.findIndex((i: Item) => i.id === item.id);
}
};
}
describe('TestDataSource', () => {
@ -18,7 +18,7 @@ describe('TestDataSource', () => {
beforeEach(() => {
dataSource = new TestDataSource();
dataSource.connect().subscribe((updated: Item[]) => {
dataSource.changes.subscribe((updated: Item[]) => {
data = updated;
});
});

View File

@ -1,8 +1,9 @@
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, Subject } from "rxjs";
export abstract class DataSource<T> {
protected data: T[] = [];
protected dataChange: BehaviorSubject<T[]> = new BehaviorSubject<T[]>([]);
protected itemUpdated: Subject<T> = new Subject<T>();
public getItems(): T[] {
return this.data;
@ -26,8 +27,10 @@ export abstract class DataSource<T> {
public update(item: T) {
const index = this.findIndex(item);
if (index >= 0) {
this.data[index] = Object.assign(this.data[index], item);
const updated = Object.assign(this.data[index], item);
this.data[index] = updated;
this.dataChange.next(this.data);
this.itemUpdated.next(updated);
}
}
@ -39,10 +42,14 @@ export abstract class DataSource<T> {
}
}
public connect() {
public get changes() {
return this.dataChange;
}
public get itemChanged() {
return this.itemUpdated;
}
public clear() {
this.data = [];
this.dataChange.next(this.data);

View File

@ -8,7 +8,7 @@ describe('DrawingsDataSource', () => {
beforeEach(() => {
dataSource = new DrawingsDataSource();
dataSource.connect().subscribe((drawings: Drawing[]) => {
dataSource.changes.subscribe((drawings: Drawing[]) => {
data = drawings;
});
});

View File

@ -1,5 +1,5 @@
import { LinksDataSource } from "./links-datasource";
import { Link } from "../models/link";
import { Link } from "../../models/link";
describe('LinksDataSource', () => {
@ -8,7 +8,7 @@ describe('LinksDataSource', () => {
beforeEach(() => {
dataSource = new LinksDataSource();
dataSource.connect().subscribe((links: Link[]) => {
dataSource.changes.subscribe((links: Link[]) => {
data = links;
});
});

View File

@ -1,7 +1,7 @@
import { Injectable } from "@angular/core";
import { DataSource } from "./datasource";
import { Link} from "../models/link";
import { Link} from "../../models/link";
@Injectable()

View File

@ -8,7 +8,7 @@ describe('NodesDataSource', () => {
beforeEach(() => {
dataSource = new NodesDataSource();
dataSource.connect().subscribe((nodes: Node[]) => {
dataSource.changes.subscribe((nodes: Node[]) => {
data = nodes;
});
});

View File

@ -1,5 +1,5 @@
import { SymbolsDataSource } from "./symbols-datasource";
import { Symbol } from "../models/symbol";
import { Symbol } from "../../models/symbol";
describe('SymbolsDataSource', () => {
@ -8,7 +8,7 @@ describe('SymbolsDataSource', () => {
beforeEach(() => {
dataSource = new SymbolsDataSource();
dataSource.connect().subscribe((symbols: Symbol[]) => {
dataSource.changes.subscribe((symbols: Symbol[]) => {
data = symbols;
});
});

View File

@ -1,7 +1,7 @@
import { Injectable } from "@angular/core";
import { DataSource } from "./datasource";
import { Symbol } from "../models/symbol";
import { Symbol } from "../../models/symbol";
@Injectable()

View File

@ -0,0 +1,12 @@
import { Node } from "../models/node";
import { Port } from "../../models/port";
export class LinkCreated {
constructor(
public sourceNode: Node,
public sourcePort: Port,
public targetNode: Node,
public targetPort: Port
){}
}

View File

@ -0,0 +1,14 @@
import { Node } from "../models/node";
class NodeEvent {
constructor(
public event: any,
public node: Node
) {}
}
export class NodeDragging extends NodeEvent {}
export class NodeDragged extends NodeEvent {}
export class NodeClicked extends NodeEvent {}
export class NodeContextMenu extends NodeEvent {}

View File

@ -1,5 +1,5 @@
import { InRectangleHelper } from "./in-rectangle-helper";
import { Rectangle } from "../../../models/rectangle";
import { Rectangle } from "../models/rectangle";

View File

@ -1,7 +1,6 @@
import { Injectable } from "@angular/core";
import { Selectable } from "../../../managers/selection-manager";
import { Rectangle } from "../../../models/rectangle";
import { Rectangle } from "../models/rectangle";
@Injectable()

View File

@ -1,5 +1,8 @@
import {Link} from "../../../models/link";
import { Injectable } from "@angular/core";
import { Link } from "../../models/link";
@Injectable()
export class MultiLinkCalculatorHelper {
LINK_WIDTH = 2;

View File

@ -1,7 +1,7 @@
import { LayersManager } from "./layers-manager";
import { Node } from "../models/node";
import { Drawing } from "../models/drawing";
import { Link } from "../models/link";
import { Link } from "../../models/link";
describe('LayersManager', () => {

View File

@ -1,10 +1,13 @@
import { Injectable } from "@angular/core";
import { Layer } from "../models/layer";
import { Node } from "../models/node";
import { Drawing } from "../models/drawing";
import { Link } from "../models/link";
import { Link } from "../../models/link";
import { Dictionary } from "../models/types";
@Injectable()
export class LayersManager {
private layers: Dictionary<Layer>;
@ -57,4 +60,5 @@ export class LayersManager {
}
return this.layers[key];
}
}

View File

@ -1,13 +1,13 @@
import { Subject} from "rxjs";
import { Node } from "../models/node";
import { Link } from "../models/link";
import { Link } from "../../models/link";
import { Drawing } from "../models/drawing";
import { Rectangle } from "../models/rectangle";
import { SelectionManager } from "./selection-manager";
import { NodesDataSource } from "../datasources/nodes-datasource";
import { LinksDataSource } from "../datasources/links-datasource";
import { InRectangleHelper } from "../components/map/helpers/in-rectangle-helper";
import { InRectangleHelper } from "../helpers/in-rectangle-helper";
import { DrawingsDataSource } from "../datasources/drawings-datasource";

View File

@ -6,9 +6,9 @@ import { Subscription } from "rxjs";
import { NodesDataSource } from "../datasources/nodes-datasource";
import { LinksDataSource } from "../datasources/links-datasource";
import { Node } from "../models/node";
import { InRectangleHelper } from "../components/map/helpers/in-rectangle-helper";
import { InRectangleHelper } from "../helpers/in-rectangle-helper";
import { Rectangle } from "../models/rectangle";
import { Link} from "../models/link";
import { Link} from "../../models/link";
import { DataSource } from "../datasources/datasource";
import { Drawing } from "../models/drawing";
import { InterfaceLabel } from "../models/interface-label";
@ -38,15 +38,19 @@ export class SelectionManager {
public subscribe(subject: Subject<Rectangle>) {
this.subscription = subject.subscribe((rectangle: Rectangle) => {
this.selectedNodes = this.getSelectedItemsInRectangle<Node>(this.nodesDataSource, rectangle);
this.selectedLinks = this.getSelectedItemsInRectangle<Link>(this.linksDataSource, rectangle);
this.selectedDrawings = this.getSelectedItemsInRectangle<Drawing>(this.drawingsDataSource, rectangle);
// don't select interfaces for now
// this.selectedInterfaceLabels = this.getSelectedInterfaceLabelsInRectangle(rectangle);
this.onSelection(rectangle);
});
return this.subscription;
}
public onSelection(rectangle: Rectangle) {
this.selectedNodes = this.getSelectedItemsInRectangle<Node>(this.nodesDataSource, rectangle);
this.selectedLinks = this.getSelectedItemsInRectangle<Link>(this.linksDataSource, rectangle);
this.selectedDrawings = this.getSelectedItemsInRectangle<Drawing>(this.drawingsDataSource, rectangle);
// don't select interfaces for now
// this.selectedInterfaceLabels = this.getSelectedInterfaceLabelsInRectangle(rectangle);
}
public getSelectedNodes() {
return this.selectedNodes;
}

View File

@ -1,5 +1,6 @@
import {Size} from "./size";
import {Point} from "./point";
import { Size } from "./size";
import { Point } from "./point";
import { Injectable } from "@angular/core";
export class Transformation {
constructor(
@ -9,12 +10,13 @@ export class Transformation {
) {}
}
@Injectable()
export class Context {
public transformation: Transformation;
public size: Size;
constructor(public centerZeroZeroPoint = true) {
public centerZeroZeroPoint = true;
constructor() {
this.size = new Size(0, 0);
this.transformation = new Transformation(0, 0, 1);
}

View File

@ -0,0 +1,8 @@
export class GraphLink {
distance: number; // this is not from server
length: number; // this is not from server
source: Node; // this is not from server
target: Node; // this is not from server
x: number; // this is not from server
y: number; // this is not from server
}

View File

@ -1,16 +1,13 @@
import {Drawing} from "./drawing";
import {Link} from "./link";
import {Link} from "../../models/link";
import {Node} from "./node";
export class Layer {
index: number;
nodes: Node[];
drawings: Drawing[];
links: Link[];
constructor() {
this.nodes = [];
this.drawings = [];
this.links = [];
constructor(
public index?: number,
public nodes: Node[] = [],
public drawings: Drawing[] = [],
public links: Link[] = []
) {
}
}

View File

@ -1,6 +1,6 @@
import {Label} from "./label";
import {Port} from "../../models/port";
import {Selectable} from "../managers/selection-manager";
import { Label } from "./label";
import { Port } from "../../models/port";
import { Selectable } from "../managers/selection-manager";
export class Node implements Selectable {

View File

@ -1,3 +1,6 @@
export class Point {
constructor(public x: number, public y: number) {}
constructor(
public x?: number,
public y?: number
) {}
};

View File

@ -1,3 +1,5 @@
export class Size {
constructor(public width: number, public height: number) {}
constructor(
public width: number,
public height: number) {}
}

View File

@ -0,0 +1,15 @@
import { MapChangeDetectorRef } from "./map-change-detector-ref";
describe('MapChangeDetectorRef', () => {
let detector: MapChangeDetectorRef;
beforeEach(() => {
detector = new MapChangeDetectorRef();
});
it("should emit event", () => {
spyOn(detector.changesDetected, 'emit');
detector.detectChanges();
expect(detector.changesDetected.emit).toHaveBeenCalledWith(true);
})
});

View File

@ -0,0 +1,11 @@
import { Injectable, EventEmitter } from "@angular/core";
@Injectable()
export class MapChangeDetectorRef {
public changesDetected = new EventEmitter<boolean>();
public detectChanges() {
this.changesDetected.emit(true);
}
}

View File

@ -12,7 +12,9 @@ describe('MovingTool', () => {
let node: SVGSelection;
beforeEach(() => {
tool = new MovingTool();
context = new Context();
tool = new MovingTool(context);
svg = new TestSVGCanvas();
node = svg.canvas
@ -21,11 +23,8 @@ describe('MovingTool', () => {
.attr('x', 10)
.attr('y', 20);
context = new Context();
tool.connect(svg.svg, context);
tool.setEnabled(true);
tool.draw(svg.svg, context);
tool.activate();
});
afterEach(() => {
@ -86,7 +85,8 @@ describe('MovingTool', () => {
describe('MovingTool can be deactivated', () => {
beforeEach(() => {
tool.deactivate();
tool.setEnabled(false);
tool.draw(svg.svg, context);
svg.svg.node().dispatchEvent(
new MouseEvent('mousedown', {

View File

@ -1,3 +1,5 @@
import { Injectable } from "@angular/core";
import { D3ZoomEvent, zoom, ZoomBehavior} from "d3-zoom";
import { event } from "d3-selection";
@ -5,33 +7,49 @@ import { SVGSelection} from "../models/types";
import { Context} from "../models/context";
@Injectable()
export class MovingTool {
private selection: SVGSelection;
private context: Context;
private zoom: ZoomBehavior<SVGSVGElement, any>;
constructor() {
private enabled = false;
private needsDeactivate = false;
private needsActivate = false;
constructor(
private context: Context
) {
this.zoom = zoom<SVGSVGElement, any>()
.scaleExtent([1 / 2, 8]);
}
public connect(selection: SVGSelection, context: Context) {
this.selection = selection;
this.context = context;
public setEnabled(enabled) {
if (this.enabled != enabled) {
if (enabled) {
this.needsActivate = true;
}
else {
this.needsDeactivate = true;
}
}
this.enabled = enabled;
}
public draw(selection: SVGSelection, context: Context) {
this.selection = selection;
this.context = context;
if(this.needsActivate) {
this.activate(selection);
this.needsActivate = false;
}
if(this.needsDeactivate) {
this.deactivate(selection);
this.needsDeactivate = false;
}
}
public activate() {
private activate(selection: SVGSelection) {
const self = this;
const onZoom = function(this: SVGSVGElement) {
const canvas = self.selection.select<SVGGElement>("g.canvas");
const canvas = selection.select<SVGGElement>("g.canvas");
const e: D3ZoomEvent<SVGSVGElement, any> = event;
canvas.attr(
'transform',
@ -48,12 +66,12 @@ export class MovingTool {
};
this.zoom.on('zoom', onZoom);
this.selection.call(this.zoom);
selection.call(this.zoom);
}
public deactivate() {
private deactivate(selection: SVGSelection) {
// d3.js preserves event `mousedown.zoom` and blocks selection
this.selection.on('mousedown.zoom', null);
selection.on('mousedown.zoom', null);
this.zoom.on('zoom', null);
}
}

View File

@ -16,18 +16,17 @@ describe('SelectionTool', () => {
let selected_rectangle: Rectangle;
beforeEach(() => {
tool = new SelectionTool();
context = new Context();
tool = new SelectionTool(context);
tool.rectangleSelected.subscribe((rectangle: Rectangle) => {
selected_rectangle = rectangle;
});
svg = new TestSVGCanvas();
context = new Context();
tool.connect(svg.svg, context);
tool.setEnabled(true);
tool.draw(svg.svg, context);
tool.activate();
selection_line_tool = svg.svg.select('g.selection-line-tool');
path_selection = selection_line_tool.select('path.selection');
});
@ -105,7 +104,9 @@ describe('SelectionTool', () => {
describe('SelectionTool can be deactivated', () => {
beforeEach(() => {
tool.deactivate();
tool.setEnabled(false);
tool.draw(svg.svg, context);
svg.svg.node().dispatchEvent(new MouseEvent('mousedown', {clientX: 100, clientY: 100}));
});

View File

@ -11,26 +11,33 @@ import { Rectangle } from "../models/rectangle";
export class SelectionTool {
static readonly SELECTABLE_CLASS = '.selectable';
public rectangleSelected: Subject<Rectangle>;
public rectangleSelected = new Subject<Rectangle>();
private selection: SVGSelection;
private path;
private context: Context;
private enabled = false;
private needsDeactivate = false;
private needsActivate = false;
public constructor() {
this.rectangleSelected = new Subject<Rectangle>();
public constructor(
private context: Context
) {}
public setEnabled(enabled) {
if (this.enabled != enabled) {
if (enabled) {
this.needsActivate = true;
}
else {
this.needsDeactivate = true;
}
}
this.enabled = enabled;
}
public connect(selection: SVGSelection, context: Context) {
this.selection = selection;
this.context = context;
}
public activate() {
private activate(selection) {
const self = this;
this.selection.on("mousedown", function() {
selection.on("mousedown", function() {
const subject = select(window);
const parent = this.parentElement;
@ -38,7 +45,7 @@ export class SelectionTool {
self.startSelection(start);
// clear selection
self.selection
selection
.selectAll(SelectionTool.SELECTABLE_CLASS)
.classed("selected", false);
@ -56,8 +63,8 @@ export class SelectionTool {
});
}
public deactivate() {
this.selection.on('mousedown', null);
private deactivate(selection) {
selection.on('mousedown', null);
}
public draw(selection: SVGSelection, context: Context) {
@ -72,7 +79,15 @@ export class SelectionTool {
.attr("class", "selection")
.attr("visibility", "hidden");
}
this.selection = selection;
if(this.needsActivate) {
this.activate(selection);
this.needsActivate = false;
}
if(this.needsDeactivate) {
this.deactivate(selection);
this.needsDeactivate = false;
}
}
private startSelection(start) {

View File

@ -1,3 +1,5 @@
import { Injectable } from "@angular/core";
import { line } from "d3-shape";
import { mouse } from "d3-selection";
@ -7,12 +9,13 @@ import { Point } from "../models/point";
import { Context } from "../models/context";
@Injectable()
export class DrawingLineWidget {
private drawingLine: DrawingLine = new DrawingLine();
private selection: SVGSelection;
private drawing = false;
private data = {};
public start(x: number, y: number, data: {}) {
const self = this;

View File

@ -1,3 +1,5 @@
import { Injectable } from "@angular/core";
import { Widget } from "./widget";
import { Drawing } from "../models/drawing";
import { SVGSelection } from "../models/types";
@ -11,15 +13,27 @@ import { EllipseDrawingWidget } from "./drawings/ellipse-drawing";
import { DrawingWidget } from "./drawings/drawing-widget";
@Injectable()
export class DrawingsWidget implements Widget {
private svgToDrawingConverter: SvgToDrawingConverter;
private drawingWidgets: DrawingWidget[] = [
new TextDrawingWidget(), new ImageDrawingWidget(), new RectDrawingWidget(),
new LineDrawingWidget(), new EllipseDrawingWidget()
];
private drawingWidgets: DrawingWidget[] = [];
constructor() {
constructor(
private svgToDrawingConverter: SvgToDrawingConverter,
private textDrawingWidget: TextDrawingWidget,
private imageDrawingWidget: ImageDrawingWidget,
private rectDrawingWidget: RectDrawingWidget,
private lineDrawingWidget: LineDrawingWidget,
private ellipseDrawingWidget: EllipseDrawingWidget
) {
this.svgToDrawingConverter = new SvgToDrawingConverter();
this.drawingWidgets = [
this.textDrawingWidget,
this.imageDrawingWidget,
this.rectDrawingWidget,
this.lineDrawingWidget,
this.ellipseDrawingWidget
];
}
public draw(view: SVGSelection, drawings?: Drawing[]) {

View File

@ -2,6 +2,7 @@ import { TestSVGCanvas } from "../../testing";
import { Drawing } from "../../models/drawing";
import { EllipseDrawingWidget } from "./ellipse-drawing";
import { EllipseElement } from "../../models/drawings/ellipse-element";
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
describe('EllipseDrawingWidget', () => {
@ -13,7 +14,7 @@ describe('EllipseDrawingWidget', () => {
beforeEach(() => {
svg = new TestSVGCanvas();
drawing = new Drawing();
widget = new EllipseDrawingWidget();
widget = new EllipseDrawingWidget(new QtDasharrayFixer());
});
afterEach(() => {

View File

@ -1,3 +1,5 @@
import { Injectable } from "@angular/core";
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { EllipseElement } from "../../models/drawings/ellipse-element";
@ -5,12 +7,12 @@ import { DrawingWidget } from "./drawing-widget";
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
@Injectable()
export class EllipseDrawingWidget implements DrawingWidget {
private qtDasharrayFixer: QtDasharrayFixer;
constructor() {
this.qtDasharrayFixer = new QtDasharrayFixer();
}
constructor(
private qtDasharrayFixer: QtDasharrayFixer
) {}
public draw(view: SVGSelection) {
const drawing = view
@ -22,7 +24,7 @@ export class EllipseDrawingWidget implements DrawingWidget {
const drawing_enter = drawing
.enter()
.append<SVGEllipseElement>('ellipse')
.attr('class', 'ellipse_element noselect');
.attr('class', 'ellipse_element noselect');
const merge = drawing.merge(drawing_enter);

View File

@ -1,9 +1,12 @@
import { Injectable } from "@angular/core";
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { ImageElement } from "../../models/drawings/image-element";
import { DrawingWidget } from "./drawing-widget";
@Injectable()
export class ImageDrawingWidget implements DrawingWidget {
public draw(view: SVGSelection) {
const drawing = view

View File

@ -2,6 +2,7 @@ import { TestSVGCanvas } from "../../testing";
import { Drawing } from "../../models/drawing";
import { LineDrawingWidget } from "./line-drawing";
import { LineElement } from "../../models/drawings/line-element";
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
describe('LineDrawingWidget', () => {
@ -13,7 +14,7 @@ describe('LineDrawingWidget', () => {
beforeEach(() => {
svg = new TestSVGCanvas();
drawing = new Drawing();
widget = new LineDrawingWidget();
widget = new LineDrawingWidget(new QtDasharrayFixer());
});
afterEach(() => {

View File

@ -1,3 +1,5 @@
import { Injectable } from "@angular/core";
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { LineElement } from "../../models/drawings/line-element";
@ -5,12 +7,12 @@ import { DrawingWidget } from "./drawing-widget";
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
@Injectable()
export class LineDrawingWidget implements DrawingWidget {
private qtDasharrayFixer: QtDasharrayFixer;
constructor() {
this.qtDasharrayFixer = new QtDasharrayFixer();
}
constructor(
private qtDasharrayFixer: QtDasharrayFixer
) {}
public draw(view: SVGSelection) {
const drawing = view

View File

@ -2,6 +2,7 @@ import { TestSVGCanvas } from "../../testing";
import { Drawing } from "../../models/drawing";
import { RectDrawingWidget } from "./rect-drawing";
import { RectElement } from "../../models/drawings/rect-element";
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
describe('RectDrawingWidget', () => {
@ -13,7 +14,7 @@ describe('RectDrawingWidget', () => {
beforeEach(() => {
svg = new TestSVGCanvas();
drawing = new Drawing();
widget = new RectDrawingWidget();
widget = new RectDrawingWidget(new QtDasharrayFixer());
});
afterEach(() => {

View File

@ -1,3 +1,5 @@
import { Injectable } from "@angular/core";
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { RectElement } from "../../models/drawings/rect-element";
@ -5,12 +7,11 @@ import { DrawingWidget } from "./drawing-widget";
import { QtDasharrayFixer } from "../../helpers/qt-dasharray-fixer";
@Injectable()
export class RectDrawingWidget implements DrawingWidget {
private qtDasharrayFixer: QtDasharrayFixer;
constructor() {
this.qtDasharrayFixer = new QtDasharrayFixer();
}
constructor(
private qtDasharrayFixer: QtDasharrayFixer
) {}
public draw(view: SVGSelection) {
const drawing = view

View File

@ -2,6 +2,7 @@ import { TestSVGCanvas } from "../../testing";
import { TextDrawingWidget } from "./text-drawing";
import { Drawing } from "../../models/drawing";
import { TextElement } from "../../models/drawings/text-element";
import { FontFixer } from "../../helpers/font-fixer";
describe('TextDrawingWidget', () => {
let svg: TestSVGCanvas;
@ -12,7 +13,7 @@ describe('TextDrawingWidget', () => {
beforeEach(() => {
svg = new TestSVGCanvas();
drawing = new Drawing();
widget = new TextDrawingWidget();
widget = new TextDrawingWidget(new FontFixer());
});
afterEach(() => {

View File

@ -1,3 +1,5 @@
import { Injectable } from "@angular/core";
import { SVGSelection } from "../../models/types";
import { TextElement } from "../../models/drawings/text-element";
import { Drawing } from "../../models/drawing";
@ -6,14 +8,13 @@ import { FontFixer } from "../../helpers/font-fixer";
import { select } from "d3-selection";
@Injectable()
export class TextDrawingWidget implements DrawingWidget {
static MARGIN = 4;
private fontFixer: FontFixer;
constructor() {
this.fontFixer = new FontFixer();
}
constructor(
private fontFixer: FontFixer
) {}
public draw(view: SVGSelection) {

View File

@ -1,35 +0,0 @@
import { line } from "d3-shape";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { Link } from "../models/link";
export class EthernetLinkWidget implements Widget {
public draw(view: SVGSelection, link: Link) {
const link_data = [[
[link.source.x + link.source.width / 2., link.source.y + link.source.height / 2.],
[link.target.x + link.target.width / 2., link.target.y + link.target.height / 2.]
]];
const value_line = line();
let link_path = view.select<SVGPathElement>('path');
link_path.classed('selected', (l: Link) => l.is_selected);
if (!link_path.node()) {
link_path = view.append<SVGPathElement>('path');
}
const link_path_data = link_path.data(link_data);
link_path_data
.attr('d', value_line)
.attr('stroke', '#000')
.attr('stroke-width', '2');
}
}

View File

@ -1,6 +1,6 @@
import { Context } from "../models/context";
import { Node } from "../models/node";
import { Link } from "../models/link";
import { Link } from "../../models/link";
import { NodesWidget } from "./nodes";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
@ -12,29 +12,24 @@ import { SelectionTool } from "../tools/selection-tool";
import { MovingTool } from "../tools/moving-tool";
import { LayersWidget } from "./layers";
import { LayersManager } from "../managers/layers-manager";
import { Injectable } from "@angular/core";
@Injectable()
export class GraphLayout implements Widget {
private nodes: Node[] = [];
private links: Link[] = [];
private drawings: Drawing[] = [];
private linksWidget: LinksWidget;
private nodesWidget: NodesWidget;
private drawingsWidget: DrawingsWidget;
private drawingLineTool: DrawingLineWidget;
private selectionTool: SelectionTool;
private movingTool: MovingTool;
private layersWidget: LayersWidget;
constructor() {
this.linksWidget = new LinksWidget();
this.nodesWidget = new NodesWidget();
this.drawingsWidget = new DrawingsWidget();
this.drawingLineTool = new DrawingLineWidget();
this.selectionTool = new SelectionTool();
this.movingTool = new MovingTool();
this.layersWidget = new LayersWidget();
constructor(
private linksWidget: LinksWidget,
private nodesWidget: NodesWidget,
private drawingsWidget: DrawingsWidget,
private drawingLineTool: DrawingLineWidget,
private selectionTool: SelectionTool,
private movingTool: MovingTool,
private layersWidget: LayersWidget
) {
}
public setNodes(nodes: Node[]) {
@ -53,32 +48,16 @@ export class GraphLayout implements Widget {
return this.nodesWidget;
}
public getLinksWidget() {
return this.linksWidget;
}
public getDrawingsWidget() {
return this.drawingsWidget;
}
public getDrawingLineTool() {
return this.drawingLineTool;
}
public getMovingTool() {
return this.movingTool;
}
public getSelectionTool() {
return this.selectionTool;
}
connect(view: SVGSelection, context: Context) {
this.drawingLineTool.connect(view, context);
this.selectionTool.connect(view, context);
this.movingTool.connect(view, context);
this.selectionTool.activate();
}
draw(view: SVGSelection, context: Context) {
@ -105,12 +84,12 @@ export class GraphLayout implements Widget {
return `translate(${xTrans}, ${yTrans}) scale(${kTrans})`;
});
// @fix me
const layersManager = new LayersManager();
layersManager.setNodes(this.nodes);
layersManager.setDrawings(this.drawings);
layersManager.setLinks(this.links);
this.layersWidget.graphLayout = this;
this.layersWidget.draw(canvas, layersManager.getLayersList());
this.drawingLineTool.draw(view, context);

View File

@ -2,11 +2,12 @@ import { Selection } from "d3-selection";
import { TestSVGCanvas } from "../testing";
import { Node } from "../models/node";
import { Link } from "../models/link";
import { LinkNode } from "../models/link-node";
import { Link } from "../../models/link";
import { LinkNode } from "../../models/link-node";
import { Label } from "../models/label";
import { InterfaceLabel } from "../models/interface-label";
import { InterfaceLabelWidget } from "./interface-label";
import { CssFixer } from "../helpers/css-fixer";
describe('InterfaceLabelsWidget', () => {
@ -66,7 +67,7 @@ describe('InterfaceLabelsWidget', () => {
.exit()
.remove();
widget = new InterfaceLabelWidget();
widget = new InterfaceLabelWidget(new CssFixer());
});
afterEach(() => {

View File

@ -1,18 +1,20 @@
import { Injectable } from "@angular/core";
import { SVGSelection } from "../models/types";
import { Link } from "../models/link";
import { Link } from "../../models/link";
import { InterfaceLabel } from "../models/interface-label";
import { CssFixer } from "../helpers/css-fixer";
import { select } from "d3-selection";
@Injectable()
export class InterfaceLabelWidget {
static SURROUNDING_TEXT_BORDER = 5;
private cssFixer: CssFixer;
private enabled = true;
constructor() {
this.cssFixer = new CssFixer();
constructor(
private cssFixer: CssFixer
) {
}
public setEnabled(enabled: boolean) {
@ -27,7 +29,7 @@ export class InterfaceLabelWidget {
const sourceInterface = new InterfaceLabel(
l.link_id,
'source',
Math.round( l.source.x + l.nodes[0].label.x),
Math.round(l.source.x + l.nodes[0].label.x),
Math.round(l.source.y + l.nodes[0].label.y),
l.nodes[0].label.text,
l.nodes[0].label.style,

View File

@ -0,0 +1,76 @@
import { Injectable } from "@angular/core";
import { select } from "d3-selection";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { Link } from "../../models/link";
import { LinkStatus } from "../models/link-status";
@Injectable()
export class InterfaceStatusWidget implements Widget {
constructor() {}
public draw(view: SVGSelection) {
view.each(function (this: SVGGElement, l: Link) {
const link_group = select<SVGGElement, Link>(this);
const link_path = link_group.select<SVGPathElement>('path');
const start_point: SVGPoint = link_path.node().getPointAtLength(45);
const end_point: SVGPoint = link_path.node().getPointAtLength(link_path.node().getTotalLength() - 45);
let statuses = [];
if (link_path.node().getTotalLength() > 2 * 45 + 10) {
statuses = [
new LinkStatus(start_point.x, start_point.y, l.source.status),
new LinkStatus(end_point.x, end_point.y, l.target.status)
];
}
const status_started = link_group
.selectAll<SVGCircleElement, LinkStatus>('circle.status_started')
.data(statuses.filter((link_status: LinkStatus) => link_status.status === 'started'));
const status_started_enter = status_started
.enter()
.append<SVGCircleElement>('circle');
status_started
.merge(status_started_enter)
.attr('class', 'status_started')
.attr('cx', (ls: LinkStatus) => ls.x)
.attr('cy', (ls: LinkStatus) => ls.y)
.attr('r', 6)
.attr('fill', '#2ecc71');
status_started
.exit()
.remove();
const status_stopped = link_group
.selectAll<SVGRectElement, LinkStatus>('rect.status_stopped')
.data(statuses.filter((link_status: LinkStatus) => link_status.status === 'stopped'));
const status_stopped_enter = status_stopped
.enter()
.append<SVGRectElement>('rect');
const STOPPED_STATUS_RECT_WIDTH = 10;
status_stopped
.merge(status_stopped_enter)
.attr('class', 'status_stopped')
.attr('x', (ls: LinkStatus) => ls.x - STOPPED_STATUS_RECT_WIDTH / 2.)
.attr('y', (ls: LinkStatus) => ls.y - STOPPED_STATUS_RECT_WIDTH / 2.)
.attr('width', STOPPED_STATUS_RECT_WIDTH)
.attr('height', STOPPED_STATUS_RECT_WIDTH)
.attr('fill', 'red');
status_stopped
.exit()
.remove();
});
}
}

View File

@ -1,4 +1,4 @@
import { instance, mock, when } from "ts-mockito";
import { instance, mock, when, verify } from "ts-mockito";
import { TestSVGCanvas } from "../testing";
import { LayersWidget } from "./layers";
@ -6,13 +6,11 @@ import { Layer } from "../models/layer";
import { LinksWidget } from "./links";
import { NodesWidget } from "./nodes";
import { DrawingsWidget } from "./drawings";
import { GraphLayout } from "./graph-layout";
describe('LayersWidget', () => {
let svg: TestSVGCanvas;
let widget: LayersWidget;
let mockedGraphLayout: GraphLayout;
let mockedLinksWidget: LinksWidget;
let mockedNodesWidget: NodesWidget;
let mockedDrawingsWidget: DrawingsWidget;
@ -20,16 +18,11 @@ describe('LayersWidget', () => {
beforeEach(() => {
svg = new TestSVGCanvas();
widget = new LayersWidget();
mockedGraphLayout = mock(GraphLayout);
mockedLinksWidget = mock(LinksWidget);
mockedNodesWidget = mock(NodesWidget);
mockedDrawingsWidget = mock(DrawingsWidget);
when(mockedGraphLayout.getLinksWidget()).thenReturn(instance(mockedLinksWidget));
when(mockedGraphLayout.getNodesWidget()).thenReturn(instance(mockedNodesWidget));
when(mockedGraphLayout.getDrawingsWidget()).thenReturn(instance(mockedDrawingsWidget));
mockedLinksWidget = instance(mock(LinksWidget));
mockedNodesWidget = instance(mock(NodesWidget));
mockedDrawingsWidget = instance(mock(DrawingsWidget));
widget.graphLayout = instance(mockedGraphLayout);
widget = new LayersWidget(mockedLinksWidget, mockedNodesWidget, mockedDrawingsWidget);
const layer_1 = new Layer();
layer_1.index = 1;

View File

@ -1,11 +1,21 @@
import { Injectable } from "@angular/core";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { GraphLayout } from "./graph-layout";
import { Layer } from "../models/layer";
import { LinksWidget } from "./links";
import { NodesWidget } from "./nodes";
import { DrawingsWidget } from "./drawings";
@Injectable()
export class LayersWidget implements Widget {
public graphLayout: GraphLayout;
constructor(
private linksWidget: LinksWidget,
private nodesWidget: NodesWidget,
private drawingsWidget: DrawingsWidget
) {}
public draw(view: SVGSelection, layers: Layer[]) {
@ -53,17 +63,9 @@ export class LayersWidget implements Widget {
.exit()
.remove();
this.graphLayout
.getLinksWidget()
.draw(links_container);
this.graphLayout
.getNodesWidget()
.draw(nodes_container);
this.graphLayout
.getDrawingsWidget()
.draw(drawings_container);
this.linksWidget.draw(links_container);
this.nodesWidget.draw(nodes_container);
this.drawingsWidget.draw(drawings_container);
}
}

View File

@ -0,0 +1,58 @@
import { Injectable } from "@angular/core";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { Link } from "../../models/link";
import { SerialLinkWidget } from "./links/serial-link";
import { EthernetLinkWidget } from "./links/ethernet-link";
import { MultiLinkCalculatorHelper } from "../helpers/multi-link-calculator-helper";
import { InterfaceLabelWidget } from "./interface-label";
import { InterfaceStatusWidget } from "./interface-status";
@Injectable()
export class LinkWidget implements Widget {
constructor(
private multiLinkCalculatorHelper: MultiLinkCalculatorHelper,
private interfaceLabelWidget: InterfaceLabelWidget,
private interfaceStatusWidget: InterfaceStatusWidget
) {}
public getInterfaceLabelWidget() {
return this.interfaceLabelWidget;
}
public getInterfaceStatusWidget() {
return this.interfaceStatusWidget;
}
public draw(view: SVGSelection) {
const link_body = view.selectAll<SVGGElement, Link>("g.link_body")
.data((l) => [l]);
const link_body_enter = link_body.enter()
.append<SVGGElement>('g')
.attr("class", "link_body");
const link_body_merge = link_body.merge(link_body_enter)
.attr('transform', (link) => {
const translation = this.multiLinkCalculatorHelper.linkTranslation(link.distance, link.source, link.target);
return `translate (${translation.dx}, ${translation.dy})`;
});
const serial_link_widget = new SerialLinkWidget();
serial_link_widget.draw(link_body_merge);
const ethernet_link_widget = new EthernetLinkWidget();
ethernet_link_widget.draw(link_body_merge);
link_body_merge
.select<SVGPathElement>('path')
.classed('selected', (l: Link) => l.is_selected);
this.getInterfaceLabelWidget().draw(link_body_merge);
this.getInterfaceStatusWidget().draw(link_body_merge);
}
}

View File

@ -1,4 +1,4 @@
import { anything, instance, mock, verify } from "ts-mockito";
import { instance, mock } from "ts-mockito";
import { Selection } from "d3-selection";
@ -6,8 +6,9 @@ import { TestSVGCanvas } from "../testing";
import { Layer } from "../models/layer";
import { LinksWidget } from "./links";
import { Node } from "../models/node";
import { Link } from "../models/link";
import { InterfaceLabelWidget } from "./interface-label";
import { Link } from "../../models/link";
import { LinkWidget } from "./link";
import { MultiLinkCalculatorHelper } from "../helpers/multi-link-calculator-helper";
describe('LinksWidget', () => {
@ -15,10 +16,12 @@ describe('LinksWidget', () => {
let widget: LinksWidget;
let layersEnter: Selection<SVGGElement, Layer, SVGGElement, any>;
let layer: Layer;
let mockedLinkWidget: LinkWidget;
beforeEach(() => {
svg = new TestSVGCanvas();
widget = new LinksWidget();
mockedLinkWidget = instance(mock(LinkWidget));
widget = new LinksWidget(new MultiLinkCalculatorHelper(), mockedLinkWidget);
const node_1 = new Node();
node_1.node_id = "1";
@ -62,9 +65,9 @@ describe('LinksWidget', () => {
});
it('should draw links', () => {
const interfaceLabelWidgetMock = mock(InterfaceLabelWidget);
const interfaceLabelWidget = instance(interfaceLabelWidgetMock);
spyOn(widget, 'getInterfaceLabelWidget').and.returnValue(interfaceLabelWidget);
const linkWidgetMock = mock(LinkWidget);
const linkWidget = instance(linkWidgetMock);
spyOn(widget, 'getLinkWidget').and.returnValue(linkWidget);
widget.draw(layersEnter);
@ -73,9 +76,6 @@ describe('LinksWidget', () => {
expect(linkNode.getAttribute('link_id')).toEqual('link1');
expect(linkNode.getAttribute('map-source')).toEqual('1');
expect(linkNode.getAttribute('map-target')).toEqual('2');
expect(linkNode.getAttribute('transform')).toEqual('translate (0, 0)');
verify(interfaceLabelWidgetMock.draw(anything())).called();
});
});

View File

@ -1,124 +1,29 @@
import { select } from "d3-selection";
import { Injectable } from "@angular/core";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { Link } from "../models/link";
import { LinkStatus } from "../models/link-status";
import { MultiLinkCalculatorHelper } from "../components/map/helpers/multi-link-calculator-helper";
import { SerialLinkWidget } from "./serial-link";
import { EthernetLinkWidget } from "./ethernet-link";
import { Link } from "../../models/link";
import { MultiLinkCalculatorHelper } from "../helpers/multi-link-calculator-helper";
import { Layer } from "../models/layer";
import { InterfaceLabelWidget } from "./interface-label";
import { LinkWidget } from "./link";
@Injectable()
export class LinksWidget implements Widget {
private multiLinkCalculatorHelper = new MultiLinkCalculatorHelper();
private interfaceLabelWidget: InterfaceLabelWidget;
constructor() {
this.interfaceLabelWidget = new InterfaceLabelWidget();
constructor(
private multiLinkCalculatorHelper: MultiLinkCalculatorHelper,
private linkWidget: LinkWidget
) {
}
public getInterfaceLabelWidget() {
return this.interfaceLabelWidget;
public getLinkWidget() {
return this.linkWidget;
}
public setInterfaceLabelWidget(interfaceLabelWidget: InterfaceLabelWidget) {
this.interfaceLabelWidget = interfaceLabelWidget;
public redrawLink(view: SVGSelection, link: Link) {
this.getLinkWidget().draw(this.selectLink(view, link));
}
public getLinkWidget(link: Link) {
if (link.link_type === 'serial') {
return new SerialLinkWidget();
}
return new EthernetLinkWidget();
}
public select(view: SVGSelection) {
return view.selectAll<SVGGElement, Link>("g.link");
}
public revise(selection: SVGSelection) {
const self = this;
selection
.each(function (this: SVGGElement, l: Link) {
const link_group = select<SVGGElement, Link>(this);
const link_widget = self.getLinkWidget(l);
link_widget.draw(link_group, l);
const link_path = link_group.select<SVGPathElement>('path');
const start_point: SVGPoint = link_path.node().getPointAtLength(45);
const end_point: SVGPoint = link_path.node().getPointAtLength(link_path.node().getTotalLength() - 45);
let statuses = [];
if (link_path.node().getTotalLength() > 2 * 45 + 10) {
statuses = [
new LinkStatus(start_point.x, start_point.y, l.source.status),
new LinkStatus(end_point.x, end_point.y, l.target.status)
];
}
const status_started = link_group
.selectAll<SVGCircleElement, LinkStatus>('circle.status_started')
.data(statuses.filter((link_status: LinkStatus) => link_status.status === 'started'));
const status_started_enter = status_started
.enter()
.append<SVGCircleElement>('circle');
status_started
.merge(status_started_enter)
.attr('class', 'status_started')
.attr('cx', (ls: LinkStatus) => ls.x)
.attr('cy', (ls: LinkStatus) => ls.y)
.attr('r', 6)
.attr('fill', '#2ecc71');
status_started
.exit()
.remove();
const status_stopped = link_group
.selectAll<SVGRectElement, LinkStatus>('rect.status_stopped')
.data(statuses.filter((link_status: LinkStatus) => link_status.status === 'stopped'));
const status_stopped_enter = status_stopped
.enter()
.append<SVGRectElement>('rect');
const STOPPED_STATUS_RECT_WIDTH = 10;
status_stopped
.merge(status_stopped_enter)
.attr('class', 'status_stopped')
.attr('x', (ls: LinkStatus) => ls.x - STOPPED_STATUS_RECT_WIDTH / 2.)
.attr('y', (ls: LinkStatus) => ls.y - STOPPED_STATUS_RECT_WIDTH / 2.)
.attr('width', STOPPED_STATUS_RECT_WIDTH)
.attr('height', STOPPED_STATUS_RECT_WIDTH)
.attr('fill', 'red');
status_stopped
.exit()
.remove();
})
.attr('transform', function(l) {
if (l.source && l.target) {
const translation = self.multiLinkCalculatorHelper.linkTranslation(l.distance, l.source, l.target);
return `translate (${translation.dx}, ${translation.dy})`;
}
return null;
});
this.getInterfaceLabelWidget().draw(selection);
}
public draw(view: SVGSelection, links?: Link[]) {
public draw(view: SVGSelection) {
const link = view
.selectAll<SVGGElement, Link>("g.link")
.data((layer: Layer) => {
@ -143,12 +48,14 @@ export class LinksWidget implements Widget {
const merge = link.merge(link_enter);
this.revise(merge);
this.getLinkWidget().draw(merge);
link
.exit()
.remove();
}
private selectLink(view: SVGSelection, link: Link) {
return view.selectAll<SVGGElement, Link>(`g.link[link_id="${link.link_id}"]`);
}
}

View File

@ -0,0 +1,52 @@
import { path } from "d3-path";
import { Widget } from "../widget";
import { SVGSelection } from "../../models/types";
import { Link } from "../../../models/link";
class EthernetLinkPath {
constructor(
public source: [number, number],
public target: [number, number]
) {
}
}
export class EthernetLinkWidget implements Widget {
private linktoEthernetLink(link: Link) {
return new EthernetLinkPath(
[link.source.x + link.source.width / 2., link.source.y + link.source.height / 2.],
[link.target.x + link.target.width / 2., link.target.y + link.target.height / 2.]
);
}
public draw(view: SVGSelection) {
const link = view
.selectAll<SVGPathElement, EthernetLinkPath>('path.ethernet_link')
.data((link) => {
if(link.link_type === 'ethernet') {
return [this.linktoEthernetLink(link)];
}
return [];
});
const link_enter = link.enter()
.append<SVGPathElement>('path')
.attr('class', 'ethernet_link');
link_enter
.attr('stroke', '#000')
.attr('stroke-width', '2');
const link_merge = link.merge(link_enter);
link_merge
.attr('d', (ethernet) => {
const line_generator = path();
line_generator.moveTo(ethernet.source[0], ethernet.source[1]);
line_generator.lineTo(ethernet.target[0], ethernet.target[1]);
return line_generator.toString();
});
}
}

View File

@ -0,0 +1,92 @@
import { path } from "d3-path";
import { Widget } from "../widget";
import { SVGSelection } from "../../models/types";
import { Link } from "../../../models/link";
class SerialLinkPath {
constructor(
public source: [number, number],
public source_angle: [number, number],
public target_angle: [number, number],
public target: [number, number]
) {
}
}
export class SerialLinkWidget implements Widget {
private linkToSerialLink(link: Link) {
const source = {
'x': link.source.x + link.source.width / 2,
'y': link.source.y + link.source.height / 2
};
const target = {
'x': link.target.x + link.target.width / 2,
'y': link.target.y + link.target.height / 2
};
const dx = target.x - source.x;
const dy = target.y - source.y;
const vector_angle = Math.atan2(dy, dx);
const rot_angle = -Math.PI / 4.0;
const vect_rot = [
Math.cos(vector_angle + rot_angle),
Math.sin(vector_angle + rot_angle)
];
const angle_source: [number, number] = [
source.x + dx / 2.0 + 15 * vect_rot[0],
source.y + dy / 2.0 + 15 * vect_rot[1]
];
const angle_target: [number, number] = [
target.x - dx / 2.0 - 15 * vect_rot[0],
target.y - dy / 2.0 - 15 * vect_rot[1]
];
return new SerialLinkPath(
[source.x, source.y],
angle_source,
angle_target,
[target.x, target.y]
);
}
public draw(view: SVGSelection) {
const link = view
.selectAll<SVGPathElement, SerialLinkPath>('path.serial_link')
.data((link) => {
if(link.link_type === 'serial') {
return [this.linkToSerialLink(link)];
}
return [];
});
const link_enter = link.enter()
.append<SVGPathElement>('path')
.attr('class', 'serial_link');
link_enter
.attr('stroke', '#B22222')
.attr('fill', 'none')
.attr('stroke-width', '2');
const link_merge = link.merge(link_enter);
link_merge
.attr('d', (serial) => {
const line_generator = path();
line_generator.moveTo(serial.source[0], serial.source[1]);
line_generator.lineTo(serial.source_angle[0], serial.source_angle[1]);
line_generator.lineTo(serial.target_angle[0], serial.target_angle[1]);
line_generator.lineTo(serial.target[0], serial.target[1]);
return line_generator.toString();
});
}
}

View File

@ -3,6 +3,8 @@ import { TestSVGCanvas } from "../testing";
import { NodesWidget } from "./nodes";
import { Node } from "../models/node";
import { Label } from "../models/label";
import { CssFixer } from "../helpers/css-fixer";
import { FontFixer } from "../helpers/font-fixer";
describe('NodesWidget', () => {
@ -11,7 +13,10 @@ describe('NodesWidget', () => {
beforeEach(() => {
svg = new TestSVGCanvas();
widget = new NodesWidget();
widget = new NodesWidget(
new CssFixer(),
new FontFixer()
);
});
afterEach(() => {

View File

@ -1,50 +1,37 @@
import { Injectable, EventEmitter } from "@angular/core";
import { event, select, Selection } from "d3-selection";
import { D3DragEvent, drag } from "d3-drag";
import { Widget } from "./widget";
import { Node } from "../models/node";
import { SVGSelection } from "../models/types";
import { Symbol } from "../models/symbol";
import { Symbol } from "../../models/symbol";
import { Layer } from "../models/layer";
import { CssFixer } from "../helpers/css-fixer";
import { FontFixer } from "../helpers/font-fixer";
import { NodeDragging, NodeDragged, NodeContextMenu, NodeClicked } from "../events/nodes";
@Injectable()
export class NodesWidget implements Widget {
static NODE_LABEL_MARGIN = 3;
private debug = false;
private draggingEnabled = false;
private onContextMenuCallback: (event: any, node: Node) => void;
private onNodeClickedCallback: (event: any, node: Node) => void;
private onNodeDraggedCallback: (event: any, node: Node) => void;
private onNodeDraggingCallbacks: ((event: any, node: Node) => void)[] = [];
private symbols: Symbol[] = [];
private symbols: Symbol[];
private cssFixer: CssFixer;
private fontFixer: FontFixer;
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 fontFixer: FontFixer
) {
this.symbols = [];
this.cssFixer = new CssFixer();
this.fontFixer = new FontFixer();
}
public setOnContextMenuCallback(onContextMenuCallback: (event: any, node: Node) => void) {
this.onContextMenuCallback = onContextMenuCallback;
}
public setOnNodeClickedCallback(onNodeClickedCallback: (event: any, node: Node) => void) {
this.onNodeClickedCallback = onNodeClickedCallback;
}
public setOnNodeDraggedCallback(onNodeDraggedCallback: (event: any, node: Node) => void) {
this.onNodeDraggedCallback = onNodeDraggedCallback;
}
public addOnNodeDraggingCallback(onNodeDraggingCallback: (event: any, n: Node) => void) {
this.onNodeDraggingCallbacks.push(onNodeDraggingCallback);
}
public setSymbols(symbols: Symbol[]) {
@ -55,12 +42,6 @@ export class NodesWidget implements Widget {
this.draggingEnabled = enabled;
}
private executeOnNodeDraggingCallback(callback_event: any, node: Node) {
this.onNodeDraggingCallbacks.forEach((callback: (e: any, n: Node) => void) => {
callback(callback_event, node);
});
}
public revise(selection: SVGSelection) {
selection
.attr('transform', (n: Node) => {
@ -149,14 +130,10 @@ export class NodesWidget implements Widget {
.classed('selected', (n: Node) => n.is_selected)
.on("contextmenu", function (n: Node, i: number) {
event.preventDefault();
if (self.onContextMenuCallback !== null) {
self.onContextMenuCallback(event, n);
}
self.onContextMenu.emit(new NodeContextMenu(event, n));
})
.on('click', (n: Node) => {
if (self.onNodeClickedCallback) {
self.onNodeClickedCallback(event, n);
}
this.onNodeClicked.emit(new NodeClicked(event, n));
});
// update image of node
@ -190,17 +167,15 @@ export class NodesWidget implements Widget {
n.y = e.y;
self.revise(select(this));
self.executeOnNodeDraggingCallback(event, n);
self.onNodeDragging.emit(new NodeDragging(event, n));
};
const dragging = () => {
return drag<SVGGElement, Node>()
.on('drag', callback)
.on('end', (n: Node) => {
if (self.onNodeDraggedCallback) {
const e: D3DragEvent<SVGGElement, Node, Node> = event;
self.onNodeDraggedCallback(e, n);
}
const e: D3DragEvent<SVGGElement, Node, Node> = event;
self.onNodeDragged.emit(new NodeDragged(e, n));
});
};

View File

@ -1,67 +0,0 @@
import { path } from "d3-path";
import { Widget } from "./widget";
import { SVGSelection } from "../models/types";
import { Link } from "../models/link";
export class SerialLinkWidget implements Widget {
public draw(view: SVGSelection, link: Link) {
const source = {
'x': link.source.x + link.source.width / 2,
'y': link.source.y + link.source.height / 2
};
const target = {
'x': link.target.x + link.target.width / 2,
'y': link.target.y + link.target.height / 2
};
const dx = target.x - source.x;
const dy = target.y - source.y;
const vector_angle = Math.atan2(dy, dx);
const rot_angle = -Math.PI / 4.0;
const vect_rot = [
Math.cos(vector_angle + rot_angle),
Math.sin(vector_angle + rot_angle)
];
const angle_source = [
source.x + dx / 2.0 + 15 * vect_rot[0],
source.y + dy / 2.0 + 15 * vect_rot[1]
];
const angle_target = [
target.x - dx / 2.0 - 15 * vect_rot[0],
target.y - dy / 2.0 - 15 * vect_rot[1]
];
const line_data = [
[source.x, source.y],
angle_source,
angle_target,
[target.x, target.y]
];
let link_path = view.select<SVGPathElement>('path');
if (!link_path.node()) {
link_path = view.append<SVGPathElement>('path');
}
const line_generator = path();
line_generator.moveTo(line_data[0][0], line_data[0][1]);
line_generator.lineTo(line_data[1][0], line_data[1][1]);
line_generator.lineTo(line_data[2][0], line_data[2][1]);
line_generator.lineTo(line_data[3][0], line_data[3][1]);
link_path
.attr('d', line_generator.toString())
.attr('stroke', '#B22222')
.attr('fill', 'none')
.attr('stroke-width', '2');
}
}

View File

@ -3,3 +3,14 @@ export interface Widget {
draw(view: any, datum: any): void;
}
export interface OnDraw {
draw(view: any): void;
}
export interface OnConnect {
onConnect(view: any): void;
}
export interface OnDisconnect {
onDisconnect(view: any): void;
}

View File

@ -34,18 +34,15 @@ describe('LocalServerComponent', () => {
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LocalServerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
}));
it('should create and redirect to server', fakeAsync(() => {
expect(component).toBeTruthy();
expect(serverService.getLocalServer).toHaveBeenCalled();
// @FIXME: somehow shows it's never called
// expect(router.navigate).toHaveBeenCalledWith('/server', 99, 'projects');
tick();
expect(router.navigate).toHaveBeenCalledWith(['/server', 99, 'projects']);
}));
});

View File

@ -1,6 +1,6 @@
<div class="context-menu" [style.left]="leftPosition" [style.top]="topPosition" *ngIf="node">
<span [matMenuTriggerFor]="contextMenu"></span>
<mat-menu #contextMenu="matMenu">
<mat-menu #contextMenu="matMenu" class="context-menu-items">
<app-start-node-action [server]="server" [node]="node"></app-start-node-action>
<app-stop-node-action [server]="server" [node]="node"></app-stop-node-action>
<app-move-layer-up-action *ngIf="!projectService.isReadOnly(project)" [server]="server" [node]="node"></app-move-layer-up-action>

View File

@ -18,8 +18,8 @@ export class NodeContextMenuComponent implements OnInit {
@ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger;
private topPosition;
private leftPosition;
protected topPosition;
protected leftPosition;
public node: Node;
constructor(

View File

@ -4,7 +4,7 @@ import { inject } from "@angular/core/testing";
import { mock, instance, capture, when } from "ts-mockito";
import { HotkeyModule, HotkeysService, Hotkey } from "angular2-hotkeys";
import { Observable, of } from "rxjs";
import { of } from "rxjs";
import { ProjectMapShortcutsComponent } from './project-map-shortcuts.component';
import { ToasterService, } from "../../../services/toaster.service";
@ -64,7 +64,7 @@ describe('ProjectMapShortcutsComponent', () => {
component.ngOnInit();
const [hotkey] = capture(hotkeyServiceMock.add).last();
expect((hotkey as Hotkey).combo).toEqual([ 'del' ]);
expect((hotkey as Hotkey).callback).toEqual(component.onDeleteHandler);
expect((hotkey as Hotkey).callback).toBeDefined();
});
it('should remove binding', () => {

View File

@ -28,7 +28,10 @@ export class ProjectMapShortcutsComponent implements OnInit, OnDestroy {
) { }
ngOnInit() {
this.deleteHotkey = new Hotkey('del', this.onDeleteHandler);
const self = this;
this.deleteHotkey = new Hotkey('del', (event: KeyboardEvent) => {
return self.onDeleteHandler(event);
});
this.hotkeysService.add(this.deleteHotkey);
}

View File

@ -31,13 +31,14 @@ g.node:hover {
left: 50%;
}
/*g.node text {*/
/*font-family: Roboto !important;*/
/*}*/
svg.map image:hover, svg.map image.chosen, g.selected {
-webkit-filter: grayscale(100%);
-moz-filter: grayscale(100%);
-ms-filter: grayscale(100%);
-o-filter: grayscale(100%);
filter: grayscale(100%);
filter: gray;
filter: url("#grayscale"); /* Chrome doesn't support CSS filters on SVG */
}
path.selected {
@ -81,3 +82,14 @@ g.node text,
padding-right: 15px;
}
.context-menu-items .mat-menu-item {
line-height: 24px !important;
height: 24px !important;
font-size: 13px !important;
padding: 0 6px;
}
.context-menu-items .mat-menu-item .mat-icon {
margin-right: 3px;
}

View File

@ -1,6 +1,18 @@
<div *ngIf="project" class="project-map">
<app-map [symbols]="symbols" [nodes]="nodes" [links]="links" [drawings]="drawings" [width]="project.scene_width" [height]="project.scene_height"></app-map>
<app-map
[symbols]="symbols"
[nodes]="nodes"
[links]="links"
[drawings]="drawings"
[width]="project.scene_width"
[height]="project.scene_height"
[show-interface-labels]="project.show_interface_labels"
[selection-tool]="tools.selection"
[moving-tool]="tools.moving"
[draw-link-tool]="tools.draw_link"
(onNodeDragged)="onNodeDragged($event)"
(onLinkCreated)="onLinkCreated($event)"
></app-map>
<div class="project-toolbar">
<mat-toolbar color="primary" class="project-toolbar">
@ -29,39 +41,51 @@
<mat-menu #viewMenu="matMenu" [overlapTrigger]="false">
<div class="options-item">
<mat-checkbox [(ngModel)]="showIntefaceLabels" (change)="toggleShowInterfaceLabels($event.checked)">Show interface labels</mat-checkbox>
<mat-checkbox
[ngModel]="project.show_interface_labels"
(change)="toggleShowInterfaceLabels($event.checked)">
Show interface labels
</mat-checkbox>
</div>
</mat-menu>
<mat-toolbar-row *ngIf="!readonly">
<button mat-icon-button [color]="drawLineMode ? 'primary': 'basic'" (click)="toggleDrawLineMode()">
<button mat-icon-button [color]="tools.draw_link ? 'primary': 'basic'" (click)="toggleDrawLineMode()">
<mat-icon>timeline</mat-icon>
</button>
</mat-toolbar-row>
<mat-toolbar-row>
<button mat-icon-button [color]="movingMode ? 'primary': 'basic'" (click)="toggleMovingMode()">
<button mat-icon-button [color]="tools.moving ? 'primary': 'basic'" (click)="toggleMovingMode()">
<mat-icon>zoom_out_map</mat-icon>
</button>
</mat-toolbar-row>
<mat-toolbar-row *ngIf="!readonly" >
<button mat-icon-button (click)="createSnapshotModal()">
<mat-icon>snooze</mat-icon>
</button>
<app-snapshot-menu-item
[server]="server"
[project]="project">
</app-snapshot-menu-item>
</mat-toolbar-row>
<mat-toolbar-row *ngIf="!readonly" >
<app-appliance [server]="server" (onNodeCreation)="onNodeCreation($event)"></app-appliance>
<app-appliance
[server]="server"
(onNodeCreation)="onNodeCreation($event)"
></app-appliance>
</mat-toolbar-row>
</mat-toolbar>
</div>
<app-node-context-menu [project]="project" [server]="server"></app-node-context-menu>
<app-node-select-interface (onChooseInterface)="onChooseInterface($event)"></app-node-select-interface>
</div>
<app-progress></app-progress>
<app-project-map-shortcuts *ngIf="project" [project]="project" [server]="server" [selectionManager]="selectionManager"></app-project-map-shortcuts>
<app-project-map-shortcuts
*ngIf="project"
[project]="project"
[server]="server"
[selectionManager]="selectionManager">
</app-project-map-shortcuts>

View File

@ -1,4 +1,4 @@
import { Component, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable, Subject, Subscription, from } from 'rxjs';
@ -8,33 +8,27 @@ import { map, mergeMap } from "rxjs/operators";
import { Project } from '../../models/project';
import { Node } from '../../cartography/models/node';
import { SymbolService } from '../../services/symbol.service';
import { Link } from "../../cartography/models/link";
import { Link } from "../../models/link";
import { MapComponent } from "../../cartography/components/map/map.component";
import { ServerService } from "../../services/server.service";
import { ProjectService } from '../../services/project.service';
import { Server } from "../../models/server";
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material";
import { SnapshotService } from "../../services/snapshot.service";
import { Snapshot } from "../../models/snapshot";
import { ProgressDialogService } from "../../common/progress-dialog/progress-dialog.service";
import { ProgressDialogComponent } from "../../common/progress-dialog/progress-dialog.component";
import { Drawing } from "../../cartography/models/drawing";
import { NodeContextMenuComponent } from "./node-context-menu/node-context-menu.component";
import { Appliance } from "../../models/appliance";
import { NodeService } from "../../services/node.service";
import { Symbol } from "../../cartography/models/symbol";
import { NodeSelectInterfaceComponent } from "./node-select-interface/node-select-interface.component";
import { Port } from "../../models/port";
import { Symbol } from "../../models/symbol";
import { LinkService } from "../../services/link.service";
import { ToasterService } from '../../services/toaster.service';
import { NodesDataSource } from "../../cartography/datasources/nodes-datasource";
import { LinksDataSource } from "../../cartography/datasources/links-datasource";
import { ProjectWebServiceHandler } from "../../handlers/project-web-service-handler";
import { SelectionManager } from "../../cartography/managers/selection-manager";
import { InRectangleHelper } from "../../cartography/components/map/helpers/in-rectangle-helper";
import { InRectangleHelper } from "../../cartography/helpers/in-rectangle-helper";
import { DrawingsDataSource } from "../../cartography/datasources/drawings-datasource";
import { SettingsService } from "../../services/settings.service";
import { ProgressService } from "../../common/progress/progress.service";
import { MapChangeDetectorRef } from '../../cartography/services/map-change-detector-ref';
import { NodeContextMenu, NodeDragged } from '../../cartography/events/nodes';
import { LinkCreated } from '../../cartography/events/links';
@Component({
@ -53,16 +47,20 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
public server: Server;
private ws: Subject<any>;
private drawLineMode = false;
private movingMode = false;
private readonly = false;
protected tools = {
'selection': true,
'moving': false,
'draw_link': false
};
private inReadOnlyMode = false;
protected selectionManager: SelectionManager;
@ViewChild(MapComponent) mapChild: MapComponent;
@ViewChild(NodeContextMenuComponent) nodeContextMenu: NodeContextMenuComponent;
@ViewChild(NodeSelectInterfaceComponent) nodeSelectInterfaceMenu: NodeSelectInterfaceComponent;
private subscriptions: Subscription[];
@ -71,15 +69,11 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
private serverService: ServerService,
private projectService: ProjectService,
private symbolService: SymbolService,
private snapshotService: SnapshotService,
private nodeService: NodeService,
private linkService: LinkService,
private dialog: MatDialog,
private progressDialogService: ProgressDialogService,
private progressService: ProgressService,
private toaster: ToasterService,
private projectWebServiceHandler: ProjectWebServiceHandler,
private settingsService: SettingsService,
private mapChangeDetectorRef: MapChangeDetectorRef,
protected nodesDataSource: NodesDataSource,
protected linksDataSource: LinksDataSource,
protected drawingsDataSource: DrawingsDataSource,
@ -134,29 +128,23 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
);
this.subscriptions.push(
this.drawingsDataSource.connect().subscribe((drawings: Drawing[]) => {
this.drawingsDataSource.changes.subscribe((drawings: Drawing[]) => {
this.drawings = drawings;
if (this.mapChild) {
this.mapChild.reload();
}
this.mapChangeDetectorRef.detectChanges();
})
);
this.subscriptions.push(
this.nodesDataSource.connect().subscribe((nodes: Node[]) => {
this.nodesDataSource.changes.subscribe((nodes: Node[]) => {
this.nodes = nodes;
if (this.mapChild) {
this.mapChild.reload();
}
this.mapChangeDetectorRef.detectChanges();
})
);
this.subscriptions.push(
this.linksDataSource.connect().subscribe((links: Link[]) => {
this.linksDataSource.changes.subscribe((links: Link[]) => {
this.links = links;
if (this.mapChild) {
this.mapChild.reload();
}
this.mapChangeDetectorRef.detectChanges();
})
);
}
@ -200,39 +188,24 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
}
setUpMapCallbacks(project: Project) {
if (this.readonly) {
this.mapChild.graphLayout.getSelectionTool().deactivate();
}
this.mapChild.graphLayout.getNodesWidget().setDraggingEnabled(!this.readonly);
this.mapChild.graphLayout.getNodesWidget().setOnContextMenuCallback((event: any, node: Node) => {
this.nodeContextMenu.open(node, event.clientY, event.clientX);
const onContextMenu = this.mapChild.graphLayout.getNodesWidget().onContextMenu.subscribe((eventNode: NodeContextMenu) => {
this.nodeContextMenu.open(
eventNode.node,
eventNode.event.clientY,
eventNode.event.clientX
);
});
this.mapChild.graphLayout.getNodesWidget().setOnNodeClickedCallback((event: any, node: Node) => {
this.selectionManager.clearSelection();
this.selectionManager.setSelectedNodes([node]);
if (this.drawLineMode) {
this.nodeSelectInterfaceMenu.open(node, event.clientY, event.clientX);
}
});
this.mapChild.graphLayout.getNodesWidget().setOnNodeDraggedCallback((event: any, node: Node) => {
this.nodesDataSource.update(node);
this.nodeService
.updatePosition(this.server, node, node.x, node.y)
.subscribe((n: Node) => {
this.nodesDataSource.update(n);
});
});
this.subscriptions.push(onContextMenu);
this.subscriptions.push(
this.selectionManager.subscribe(this.mapChild.graphLayout.getSelectionTool().rectangleSelected)
this.selectionManager.subscribe(
this.mapChild.graphLayout.getSelectionTool().rectangleSelected)
);
this.mapChild.graphLayout.getLinksWidget().getInterfaceLabelWidget().setEnabled(this.project.show_interface_labels);
this.mapChild.reload();
this.mapChangeDetectorRef.detectChanges();
}
onNodeCreation(appliance: Appliance) {
@ -247,76 +220,43 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
});
}
public createSnapshotModal() {
const dialogRef = this.dialog.open(CreateSnapshotDialogComponent, {
width: '250px',
});
dialogRef.afterClosed().subscribe(snapshot => {
if (snapshot) {
const creation = this.snapshotService.create(this.server, this.project.project_id, snapshot);
const progress = this.progressDialogService.open();
const subscription = creation.subscribe((created_snapshot: Snapshot) => {
this.toaster.success(`Snapshot '${snapshot.name}' has been created.`);
progress.close();
}, (response) => {
const error = response.json();
this.toaster.error(`Cannot create snapshot: ${error.message}`);
progress.close();
});
progress.afterClosed().subscribe((result) => {
if (result === ProgressDialogComponent.CANCELLED) {
subscription.unsubscribe();
}
});
}
});
onNodeDragged(nodeEvent: NodeDragged) {
this.nodesDataSource.update(nodeEvent.node);
this.nodeService
.updatePosition(this.server, nodeEvent.node, nodeEvent.node.x, nodeEvent.node.y)
.subscribe((n: Node) => {
this.nodesDataSource.update(n);
});
}
public toggleDrawLineMode() {
this.drawLineMode = !this.drawLineMode;
if (!this.drawLineMode) {
this.mapChild.graphLayout.getDrawingLineTool().stop();
public set readonly(value) {
this.inReadOnlyMode = value;
if (value) {
this.tools.selection = false;
}
else {
this.tools.selection = true;
}
}
public get readonly() {
return this.inReadOnlyMode;
}
public toggleMovingMode() {
this.movingMode = !this.movingMode;
if (this.movingMode) {
if (!this.readonly) {
this.mapChild.graphLayout.getSelectionTool().deactivate();
}
this.mapChild.graphLayout.getMovingTool().activate();
} else {
this.mapChild.graphLayout.getMovingTool().deactivate();
if (!this.readonly) {
this.mapChild.graphLayout.getSelectionTool().activate();
}
this.tools.moving = !this.tools.moving;
if (!this.readonly) {
this.tools.selection = !this.tools.moving;
}
}
public onChooseInterface(event) {
const node: Node = event.node;
const port: Port = event.port;
const drawingLineTool = this.mapChild.graphLayout.getDrawingLineTool();
if (drawingLineTool.isDrawing()) {
const data = drawingLineTool.stop();
this.onLineCreation(data['node'], data['port'], node, port);
} else {
drawingLineTool.start(node.x + node.width / 2., node.y + node.height / 2., {
'node': node,
'port': port
});
}
public toggleDrawLineMode() {
this.tools.draw_link = !this.tools.draw_link;
}
public onLineCreation(source_node: Node, source_port: Port, target_node: Node, target_port: Port) {
public onLinkCreated(linkCreated: LinkCreated) {
this.linkService
.createLink(this.server, source_node, source_port, target_node, target_port)
.createLink(this.server, linkCreated.sourceNode, linkCreated.sourcePort, linkCreated.targetNode, linkCreated.targetPort)
.subscribe(() => {
this.projectService.links(this.server, this.project.project_id).subscribe((links: Link[]) => {
this.linksDataSource.set(links);
@ -326,10 +266,6 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
public toggleShowInterfaceLabels(enabled: boolean) {
this.project.show_interface_labels = enabled;
this.mapChild.graphLayout.getLinksWidget().getInterfaceLabelWidget()
.setEnabled(this.project.show_interface_labels);
this.mapChild.reload();
}
public ngOnDestroy() {
@ -344,27 +280,3 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
}
}
@Component({
selector: 'app-create-snapshot-dialog',
templateUrl: 'create-snapshot-dialog.html',
})
export class CreateSnapshotDialogComponent {
snapshot: Snapshot = new Snapshot();
constructor(
public dialogRef: MatDialogRef<CreateSnapshotDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
}
onAddClick(): void {
this.dialogRef.close(this.snapshot);
}
onNoClick(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,9 @@
<mat-card class="info" *ngIf="discoveredServer">
<mat-card-content align="center">
We've discovered GNS3 server on <b>{{ discoveredServer.ip }}:{{ discoveredServer.port }}</b>, would you like to add to the list?
</mat-card-content>
<mat-card-actions align="right">
<button mat-button color="accent" (click)="ignore(discoveredServer)">NO</button>
<button mat-button (click)="accept(discoveredServer)">YES</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,173 @@
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import { MatCardModule } from "@angular/material";
import { Observable } from "rxjs/Rx";
import { ServerDiscoveryComponent } from './server-discovery.component';
import { VersionService } from "../../../services/version.service";
import { MockedVersionService } from "../../../services/version.service.spec";
import { Version } from "../../../models/version";
import { Server } from "../../../models/server";
import { ServerService } from '../../../services/server.service';
import { MockedServerService } from '../../../services/server.service.spec';
import { ServerDatabase } from '../../../services/server.database';
describe('ServerDiscoveryComponent', () => {
let component: ServerDiscoveryComponent;
let fixture: ComponentFixture<ServerDiscoveryComponent>;
let mockedVersionService: MockedVersionService;
let mockedServerService: MockedServerService;
beforeEach(async(() => {
mockedServerService = new MockedServerService();
mockedVersionService = new MockedVersionService();
TestBed.configureTestingModule({
imports: [ MatCardModule ],
providers: [
{ provide: VersionService, useFactory: () => mockedVersionService },
{ provide: ServerService, useFactory: () => mockedServerService },
ServerDatabase
],
declarations: [ ServerDiscoveryComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ServerDiscoveryComponent);
component = fixture.componentInstance;
// we don't really want to run it during testing
spyOn(component, 'ngOnInit').and.returnValue(null);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('isAvailable', () => {
it('should return server object when server is available', () => {
const version = new Version();
version.version = "2.1.8";
const getVersionSpy = spyOn(mockedVersionService, 'get')
.and.returnValue(Observable.of(version));
component.isServerAvailable('127.0.0.1', 3080).subscribe((s) => {
expect(s.ip).toEqual('127.0.0.1');
expect(s.port).toEqual(3080);
});
const server = new Server();
server.ip = '127.0.0.1';
server.port = 3080;
expect(getVersionSpy).toHaveBeenCalledWith(server);
});
it('should throw error once server is not available', () => {
const server = new Server();
server.ip = '127.0.0.1';
server.port = 3080;
const getVersionSpy = spyOn(mockedVersionService, 'get')
.and.returnValue(Observable.throwError(new Error("server is unavailable")));
let hasExecuted = false;
component.isServerAvailable('127.0.0.1', 3080).subscribe((ver) => {}, (err) => {
hasExecuted = true;
expect(err.toString()).toEqual('Error: server is unavailable');
});
expect(getVersionSpy).toHaveBeenCalledWith(server);
expect(hasExecuted).toBeTruthy();
});
});
describe("discovery", () => {
it('should discovery all servers available', (done) => {
const version = new Version();
version.version = "2.1.8";
spyOn(component, 'isServerAvailable').and.callFake((ip, port) => {
const server = new Server();
server.ip = ip;
server.port = port;
return Observable.of(server);
});
component.discovery().subscribe((discovered) => {
expect(discovered[0].ip).toEqual('127.0.0.1');
expect(discovered[0].port).toEqual(3080);
expect(discovered.length).toEqual(1);
done();
});
});
});
describe("discoverFirstAvailableServer", () => {
let server: Server;
beforeEach(function() {
server = new Server();
server.ip = '199.111.111.1',
server.port = 3333;
spyOn(component, 'discovery').and.callFake(() => {
return Observable.of([server]);
});
});
it('should get first server from discovered and with no added before', fakeAsync(() => {
expect(component.discoveredServer).toBeUndefined();
component.discoverFirstAvailableServer();
tick();
expect(component.discoveredServer.ip).toEqual('199.111.111.1');
expect(component.discoveredServer.port).toEqual(3333);
}));
it('should get first server from discovered and with already added', fakeAsync(() => {
mockedServerService.servers.push(server)
expect(component.discoveredServer).toBeUndefined();
component.discoverFirstAvailableServer();
tick();
expect(component.discoveredServer).toBeUndefined();
}));
});
describe("accepting and ignoring found server", () => {
let server: Server;
beforeEach(() => {
server = new Server();
server.ip = '199.111.111.1',
server.port = 3333;
component.discoveredServer = server;
});
describe("accept", () => {
it("should add new server", fakeAsync(() => {
component.accept(server);
tick();
expect(component.discoveredServer).toBeNull();
expect(mockedServerService.servers[0].ip).toEqual('199.111.111.1');
expect(mockedServerService.servers[0].name).toEqual('199.111.111.1');
}));
});
describe("ignore", () => {
it("should reject server", fakeAsync(() => {
component.ignore(server);
tick();
expect(component.discoveredServer).toBeNull();
}));
});
});
});

View File

@ -0,0 +1,92 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from "rxjs/Rx";
import { map } from "rxjs//operators";
import { Server } from "../../../models/server";
import { VersionService } from "../../../services/version.service";
import { Version } from "../../../models/version";
import { forkJoin } from 'rxjs';
import { ServerService } from '../../../services/server.service';
import { ServerDatabase } from '../../../services/server.database';
@Component({
selector: 'app-server-discovery',
templateUrl: './server-discovery.component.html',
styleUrls: ['./server-discovery.component.scss']
})
export class ServerDiscoveryComponent implements OnInit {
private defaultServers = [{
ip: '127.0.0.1',
port: 3080
}
];
discoveredServer: Server;
constructor(
private versionService: VersionService,
private serverService: ServerService,
private serverDatabase: ServerDatabase
) {}
ngOnInit() {
this.discoverFirstAvailableServer();
}
discoverFirstAvailableServer() {
forkJoin(
Observable.fromPromise(this.serverService.findAll()).pipe(map((s: Server[]) => s)),
this.discovery()
).subscribe(([local, discovered]) => {
local.forEach((added) => {
discovered = discovered.filter((server) => {
return !(server.ip == added.ip && server.port == added.port);
});
});
if(discovered.length > 0) {
this.discoveredServer = discovered.shift();
}
});
}
discovery(): Observable<Server[]> {
const queries: Observable<Server>[] = [];
this.defaultServers.forEach((testServer) => {
queries.push(this.isServerAvailable(testServer.ip, testServer.port).catch((err) => {
return Observable.of(null);
}));
});
return new Observable<Server[]>((observer) => {
forkJoin(queries).subscribe((discoveredServers) => {
observer.next(discoveredServers.filter((s) => s != null));
observer.complete();
});
});
}
isServerAvailable(ip: string, port: number): Observable<Server> {
const server = new Server();
server.ip = ip;
server.port = port;
return this.versionService.get(server).flatMap((version: Version) => Observable.of(server));
}
ignore(server: Server) {
this.discoveredServer = null;
}
accept(server: Server) {
if(server.name == null) {
server.name = server.ip;
}
this.serverService.create(server).then((created: Server) => {
this.serverDatabase.addServer(created);
this.discoveredServer = null;
});
}
}

View File

@ -3,6 +3,9 @@
<h1>Servers</h1>
</div>
<div class="default-content">
<app-server-discovery></app-server-discovery>
<mat-divider></mat-divider>
<div class="example-container mat-elevation-z8">
<mat-table #table [dataSource]="dataSource">

View File

@ -1,19 +1,14 @@
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject, OnInit, Injectable } from '@angular/core';
import { DataSource } from "@angular/cdk/collections";
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { Observable, BehaviorSubject, merge } from "rxjs";
import { map } from "rxjs/operators";
// import 'rxjs/add/operator/startWith';
// import 'rxjs/add/observable/merge';
// import 'rxjs/add/operator/map';
// import 'rxjs/add/operator/debounceTime';
// import 'rxjs/add/operator/distinctUntilChanged';
// import 'rxjs/add/observable/fromEvent';
import { Server } from "../../models/server";
import { ServerService } from "../../services/server.service";
import { ServerDatabase } from '../../services/server.database';
@Component({
@ -22,11 +17,13 @@ import { ServerService } from "../../services/server.service";
styleUrls: ['./servers.component.css']
})
export class ServersComponent implements OnInit {
serverDatabase = new ServerDatabase();
dataSource: ServerDataSource;
displayedColumns = ['id', 'name', 'ip', 'port', 'actions'];
constructor(private dialog: MatDialog, private serverService: ServerService) {}
constructor(
private dialog: MatDialog,
private serverService: ServerService,
private serverDatabase: ServerDatabase) {}
ngOnInit() {
this.serverService.findAll().then((servers: Server[]) => {
@ -89,32 +86,6 @@ export class AddServerDialogComponent implements OnInit {
}
export class ServerDatabase {
dataChange: BehaviorSubject<Server[]> = new BehaviorSubject<Server[]>([]);
get data(): Server[] {
return this.dataChange.value;
}
public addServer(server: Server) {
const servers = this.data.slice();
servers.push(server);
this.dataChange.next(servers);
}
public addServers(servers: Server[]) {
this.dataChange.next(servers);
}
public remove(server: Server) {
const index = this.data.indexOf(server);
if (index >= 0) {
this.data.splice(index, 1);
this.dataChange.next(this.data.slice());
}
}
}
export class ServerDataSource extends DataSource<Server> {
constructor(private serverDatabase: ServerDatabase) {
super();

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CreateSnapshotDialogComponent } from './create-snapshot-dialog.component';
describe('CreateSnapshotDialogComponent', () => {
let component: CreateSnapshotDialogComponent;
let fixture: ComponentFixture<CreateSnapshotDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CreateSnapshotDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CreateSnapshotDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -0,0 +1,26 @@
import { Component, Inject } from '@angular/core';
import { Snapshot } from '../../../models/snapshot';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
@Component({
selector: 'app-create-snapshot-dialog',
templateUrl: './create-snapshot-dialog.component.html',
styleUrls: ['./create-snapshot-dialog.component.scss']
})
export class CreateSnapshotDialogComponent {
snapshot: Snapshot = new Snapshot();
constructor(
public dialogRef: MatDialogRef<CreateSnapshotDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
}
onAddClick(): void {
this.dialogRef.close(this.snapshot);
}
onNoClick(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,3 @@
<button mat-icon-button (click)="createSnapshotModal()">
<mat-icon>snooze</mat-icon>
</button>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SnapshotMenuItemComponent } from './snapshot-menu-item.component';
describe('SnapshotMenuItemComponent', () => {
let component: SnapshotMenuItemComponent;
let fixture: ComponentFixture<SnapshotMenuItemComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SnapshotMenuItemComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SnapshotMenuItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -0,0 +1,60 @@
import { Component, Input, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material';
import { CreateSnapshotDialogComponent } from '../create-snapshot-dialog/create-snapshot-dialog.component';
import { SnapshotService } from '../../../services/snapshot.service';
import { ProgressDialogService } from '../../../common/progress-dialog/progress-dialog.service';
import { ToasterService } from '../../../services/toaster.service';
import { ProgressDialogComponent } from '../../../common/progress-dialog/progress-dialog.component';
import { Project } from '../../../models/project';
import { Server } from '../../../models/server';
import { Snapshot } from '../../../models/snapshot';
@Component({
selector: 'app-snapshot-menu-item',
templateUrl: './snapshot-menu-item.component.html',
styleUrls: ['./snapshot-menu-item.component.scss']
})
export class SnapshotMenuItemComponent implements OnInit {
@Input('project') project: Project;
@Input('server') server: Server;
constructor(
private dialog: MatDialog,
private snapshotService: SnapshotService,
private progressDialogService: ProgressDialogService,
private toaster: ToasterService
) { }
ngOnInit() {}
public createSnapshotModal() {
const dialogRef = this.dialog.open(CreateSnapshotDialogComponent, {
width: '250px',
});
dialogRef.afterClosed().subscribe(snapshot => {
if (snapshot) {
const creation = this.snapshotService.create(this.server, this.project.project_id, snapshot);
const progress = this.progressDialogService.open();
const subscription = creation.subscribe((created_snapshot: Snapshot) => {
this.toaster.success(`Snapshot '${snapshot.name}' has been created.`);
progress.close();
}, (response) => {
const error = response.json();
this.toaster.error(`Cannot create snapshot: ${error.message}`);
progress.close();
});
progress.afterClosed().subscribe((result) => {
if (result === ProgressDialogComponent.CANCELLED) {
subscription.unsubscribe();
}
});
}
});
}
}

View File

@ -0,0 +1,3 @@
<p>
snapshots works!
</p>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SnapshotsComponent } from './snapshots.component';
describe('SnapshotsComponent', () => {
let component: SnapshotsComponent;
let fixture: ComponentFixture<SnapshotsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SnapshotsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SnapshotsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-snapshots',
templateUrl: './snapshots.component.html',
styleUrls: ['./snapshots.component.scss']
})
export class SnapshotsComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,3 @@
export interface Converter<TSource, TDestination> {
convert(from: TSource): TDestination;
}

View File

@ -7,7 +7,7 @@ import { NodesDataSource } from "../cartography/datasources/nodes-datasource";
import { LinksDataSource } from "../cartography/datasources/links-datasource";
import { DrawingsDataSource } from "../cartography/datasources/drawings-datasource";
import { Node } from "../cartography/models/node";
import { Link } from "../cartography/models/link";
import { Link } from "../models/link";
import { Drawing } from "../cartography/models/drawing";

Some files were not shown because too many files have changed in this diff Show More