diff --git a/.circleci/config.yml b/.circleci/config.yml index 32795951..498a8547 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -36,10 +36,10 @@ jobs: brew --config brew upgrade python python -V - pip install -r scripts/requirements.txt - python scripts/build.py download - python scripts/build.py build_exe -b dist/exe.gns3server -s - python scripts/build.py validate -b dist + pip3 install -r scripts/requirements.txt + python3 scripts/build.py download + python3 scripts/build.py build_exe -b dist/exe.gns3server -s + python3 scripts/build.py validate -b dist - run: name: Dist project diff --git a/src/app/app.module.ts b/src/app/app.module.ts index cec952a9..84437feb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -53,6 +53,12 @@ import { ApplianceListDialogComponent } from './appliance/appliance-list-dialog/ import { NodeSelectInterfaceComponent } from './shared/node-select-interface/node-select-interface.component'; import { CartographyModule } from './cartography/cartography.module'; import { ToasterService } from './shared/services/toaster.service'; +import {ProjectWebServiceHandler} from "./shared/handlers/project-web-service-handler"; +import {LinksDataSource} from "./cartography/shared/datasources/links-datasource"; +import {NodesDataSource} from "./cartography/shared/datasources/nodes-datasource"; +import {SymbolsDataSource} from "./cartography/shared/datasources/symbols-datasource"; +import {SelectionManager} from "./cartography/shared/managers/selection-manager"; +import {InRectangleHelper} from "./cartography/map/helpers/in-rectangle-helper"; @NgModule({ @@ -108,7 +114,13 @@ import { ToasterService } from './shared/services/toaster.service'; HttpServer, SnapshotService, ProgressDialogService, - ToasterService + ToasterService, + ProjectWebServiceHandler, + LinksDataSource, + NodesDataSource, + SymbolsDataSource, + SelectionManager, + InRectangleHelper ], entryComponents: [ AddServerDialogComponent, diff --git a/src/app/appliance/appliance-list-dialog/appliance-list-dialog.component.ts b/src/app/appliance/appliance-list-dialog/appliance-list-dialog.component.ts index 12bbd2eb..23b0f7df 100644 --- a/src/app/appliance/appliance-list-dialog/appliance-list-dialog.component.ts +++ b/src/app/appliance/appliance-list-dialog/appliance-list-dialog.component.ts @@ -68,9 +68,11 @@ export class ApplianceDatabase { } constructor(private server: Server, private applianceService: ApplianceService) { - this.applianceService.list(this.server).subscribe((appliances: Appliance[]) => { - this.dataChange.next(appliances); - }); + this.applianceService + .list(this.server) + .subscribe((appliances) => { + this.dataChange.next(appliances); + }); } }; diff --git a/src/app/appliance/appliance.component.ts b/src/app/appliance/appliance.component.ts index cf83ee77..9b18011b 100644 --- a/src/app/appliance/appliance.component.ts +++ b/src/app/appliance/appliance.component.ts @@ -3,6 +3,7 @@ import {MatDialog} from "@angular/material"; import {ApplianceListDialogComponent} from "./appliance-list-dialog/appliance-list-dialog.component"; import {Server} from "../shared/models/server"; +import {Appliance} from "../shared/models/appliance"; @Component({ selector: 'app-appliance', @@ -26,7 +27,7 @@ export class ApplianceComponent implements OnInit { } }); - dialogRef.afterClosed().subscribe((appliance: AppendMode) => { + dialogRef.afterClosed().subscribe((appliance: Appliance) => { if (appliance !== null) { this.onNodeCreation.emit(appliance); } diff --git a/src/app/cartography/map/helpers/in-rectangle-helper.ts b/src/app/cartography/map/helpers/in-rectangle-helper.ts new file mode 100644 index 00000000..08c8936d --- /dev/null +++ b/src/app/cartography/map/helpers/in-rectangle-helper.ts @@ -0,0 +1,11 @@ +import {Selectable} from "../../shared/managers/selection-manager"; +import {Rectangle} from "../../shared/models/rectangle"; +import {Injectable} from "@angular/core"; + +@Injectable() +export class InRectangleHelper { + public inRectangle(item: Selectable, rectangle: Rectangle): boolean { + return (rectangle.x <= item.x && item.x < (rectangle.x + rectangle.width) + && rectangle.y <= item.y && item.y < (rectangle.y + rectangle.height)); + } +} diff --git a/src/app/cartography/map/helpers/multi-link-calculator-helper.ts b/src/app/cartography/map/helpers/multi-link-calculator-helper.ts index 10ff65ba..da482dc9 100644 --- a/src/app/cartography/map/helpers/multi-link-calculator-helper.ts +++ b/src/app/cartography/map/helpers/multi-link-calculator-helper.ts @@ -1,4 +1,4 @@ -import {Link} from "../../shared/models/link.model"; +import {Link} from "../../shared/models/link"; export class MultiLinkCalculatorHelper { LINK_WIDTH = 2; diff --git a/src/app/cartography/map/map.component.scss b/src/app/cartography/map/map.component.scss index 42723990..7fb7062f 100644 --- a/src/app/cartography/map/map.component.scss +++ b/src/app/cartography/map/map.component.scss @@ -2,7 +2,4 @@ svg { display: block; } -image.over { - fill: #000; -} diff --git a/src/app/cartography/map/map.component.ts b/src/app/cartography/map/map.component.ts index 056c8dab..22bcd24e 100644 --- a/src/app/cartography/map/map.component.ts +++ b/src/app/cartography/map/map.component.ts @@ -4,13 +4,13 @@ import { import { D3, D3Service } from 'd3-ng2-service'; import {select, Selection} from 'd3-selection'; -import { Node } from "../shared/models/node.model"; -import { Link } from "../shared/models/link.model"; +import { Node } from "../shared/models/node"; +import { Link } from "../shared/models/link"; import { GraphLayout } from "../shared/widgets/graph.widget"; -import { Context } from "../../map/models/context"; -import { Size } from "../shared/models/size.model"; -import { Drawing } from "../shared/models/drawing.model"; -import {Symbol} from "../../shared/models/symbol"; +import { Context } from "../shared/models/context"; +import { Size } from "../shared/models/size"; +import { Drawing } from "../shared/models/drawing"; +import {Symbol} from "../shared/models/symbol"; @Component({ @@ -68,9 +68,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { } ngOnDestroy() { - if (this.svg.empty && !this.svg.empty()) { - this.svg.selectAll('*').remove(); - } + this.graphLayout.disconnect(this.svg); } ngOnInit() { @@ -78,14 +76,12 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { let rootElement: Selection; - const self = this; - if (this.parentNativeElement !== null) { rootElement = d3.select(this.parentNativeElement); this.svg = rootElement.select('svg'); - this.graphContext = new Context(this.svg); + this.graphContext = new Context(true); if (this.windowFullSize) { this.graphContext.setSize(this.getSize()); @@ -94,6 +90,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { } this.graphLayout = new GraphLayout(); + this.graphLayout.connect(this.svg, this.graphContext); this.graphLayout.getNodesWidget().addOnNodeDraggingCallback((event: any, n: Node) => { const linksWidget = this.graphLayout.getLinksWidget(); @@ -131,8 +128,6 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { .attr('height', this.graphContext.getSize().height); } - - this.graphLayout.setNodes(this.nodes); this.graphLayout.setLinks(this.links); this.graphLayout.setDrawings(this.drawings); @@ -155,6 +150,11 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { if (target_id in nodes_by_id) { link.target = nodes_by_id[target_id]; } + + if (link.source && link.target) { + link.x = link.source.x + (link.target.x - link.source.x) * 0.5; + link.y = link.source.y + (link.target.y - link.source.y) * 0.5; + } }); } diff --git a/src/app/cartography/shared/datasources/datasource.spec.ts b/src/app/cartography/shared/datasources/datasource.spec.ts new file mode 100644 index 00000000..f60e6572 --- /dev/null +++ b/src/app/cartography/shared/datasources/datasource.spec.ts @@ -0,0 +1,73 @@ +import {DataSource} from "./datasource"; + +class Item { + constructor(public id: string, public property1?: string, public property2?: string) {} +} + + +class TestDataSource extends DataSource { + protected findIndex(item: Item) { + return this.data.findIndex((i: Item) => i.id === item.id); + } +}; + + + +describe('TestDataSource', () => { + let dataSource: TestDataSource; + let data: Item[]; + + beforeEach(() => { + dataSource = new TestDataSource(); + dataSource.connect().subscribe((updated: Item[]) => { + data = updated; + }); + }); + + describe('Item can be added', () => { + beforeEach(() => { + dataSource.add(new Item("test1", "property1")); + }); + + it('item should be in data', () => { + expect(data).toEqual([new Item("test1", "property1")]); + }); + }); + + describe('Items can be set', () => { + beforeEach(() => { + dataSource.set([new Item("test1", "property1"), new Item("test2", "property2")]); + }); + + it('items should be in data', () => { + expect(data).toEqual([new Item("test1", "property1"), new Item("test2", "property2")]); + }); + }); + + describe('Items can be removed', () => { + beforeEach(() => { + dataSource.set([new Item("test1", "property1"), new Item("test2", "property2")]); + dataSource.remove(new Item("test1", "property1")); + }); + + it('item should not be in data', () => { + expect(data).toEqual([new Item("test2", "property2")]); + }); + }); + + describe('Item can be updated', () => { + beforeEach(() => { + dataSource.set([new Item("test1", "property1", "another"), new Item("test2", "property2")]); + dataSource.update(new Item("test1", "property3")); + }); + + it('item should be updated', () => { + expect(data).toEqual([ + new Item("test1", "property3"), + new Item("test2", "property2") + ]); + }); + + }); + +}); diff --git a/src/app/cartography/shared/datasources/datasource.ts b/src/app/cartography/shared/datasources/datasource.ts new file mode 100644 index 00000000..97e31304 --- /dev/null +++ b/src/app/cartography/shared/datasources/datasource.ts @@ -0,0 +1,42 @@ +import {BehaviorSubject} from "rxjs/BehaviorSubject"; + +export abstract class DataSource { + protected data: T[] = []; + protected dataChange: BehaviorSubject = new BehaviorSubject([]); + + public getItems(): T[] { + return this.data; + } + + public add(item: T) { + this.data.push(item); + this.dataChange.next(this.data); + } + + public set(data: T[]) { + this.data = data; + this.dataChange.next(this.data); + } + + public update(item: T) { + const index = this.findIndex(item); + if (index >= 0) { + this.data[index] = Object.assign(this.data[index], item); + this.dataChange.next(this.data); + } + } + + public remove(item: T) { + const index = this.findIndex(item); + if (index >= 0) { + this.data.splice(index, 1); + this.dataChange.next(this.data); + } + } + + public connect() { + return this.dataChange; + } + + protected abstract findIndex(item: T): number; +} diff --git a/src/app/cartography/shared/datasources/links-datasource.spec.ts b/src/app/cartography/shared/datasources/links-datasource.spec.ts new file mode 100644 index 00000000..9556028c --- /dev/null +++ b/src/app/cartography/shared/datasources/links-datasource.spec.ts @@ -0,0 +1,33 @@ +import {LinksDataSource} from "./links-datasource"; +import {Link} from "../models/link"; + + +describe('LinksDataSource', () => { + let dataSource: LinksDataSource; + let data: Link[]; + + beforeEach(() => { + dataSource = new LinksDataSource(); + dataSource.connect().subscribe((links: Link[]) => { + data = links; + }); + }); + + describe('Link can be updated', () => { + beforeEach(() => { + const link = new Link(); + link.link_id = "1"; + link.project_id = "project-1"; + dataSource.add(link); + + link.project_id = "project-2"; + dataSource.update(link); + }); + + it('project_id should change', () => { + expect(data[0].link_id).toEqual("1"); + expect(data[0].project_id).toEqual("project-2"); + }); + }); + +}); diff --git a/src/app/cartography/shared/datasources/links-datasource.ts b/src/app/cartography/shared/datasources/links-datasource.ts new file mode 100644 index 00000000..cd747b43 --- /dev/null +++ b/src/app/cartography/shared/datasources/links-datasource.ts @@ -0,0 +1,12 @@ +import {Injectable} from "@angular/core"; + +import {DataSource} from "./datasource"; +import {Link} from "../models/link"; + + +@Injectable() +export class LinksDataSource extends DataSource { + protected findIndex(link: Link) { + return this.data.findIndex((l: Link) => l.link_id === link.link_id); + } +} diff --git a/src/app/cartography/shared/datasources/nodes-datasource.spec.ts b/src/app/cartography/shared/datasources/nodes-datasource.spec.ts new file mode 100644 index 00000000..fca5534b --- /dev/null +++ b/src/app/cartography/shared/datasources/nodes-datasource.spec.ts @@ -0,0 +1,33 @@ +import {NodesDataSource} from "./nodes-datasource"; +import {Node} from "../models/node"; + + +describe('NodesDataSource', () => { + let dataSource: NodesDataSource; + let data: Node[]; + + beforeEach(() => { + dataSource = new NodesDataSource(); + dataSource.connect().subscribe((nodes: Node[]) => { + data = nodes; + }); + }); + + describe('Node can be updated', () => { + beforeEach(() => { + const node = new Node(); + node.node_id = "1"; + node.name = "Node 1"; + dataSource.add(node); + + node.name = "Node 2"; + dataSource.update(node); + }); + + it('name should change', () => { + expect(data[0].node_id).toEqual("1"); + expect(data[0].name).toEqual("Node 2"); + }); + }); + +}); diff --git a/src/app/cartography/shared/datasources/nodes-datasource.ts b/src/app/cartography/shared/datasources/nodes-datasource.ts new file mode 100644 index 00000000..c5c58eb4 --- /dev/null +++ b/src/app/cartography/shared/datasources/nodes-datasource.ts @@ -0,0 +1,11 @@ +import {Node} from "../models/node"; +import {DataSource} from "./datasource"; +import {Injectable} from "@angular/core"; + + +@Injectable() +export class NodesDataSource extends DataSource { + protected findIndex(node: Node) { + return this.data.findIndex((n: Node) => n.node_id === node.node_id); + } +} diff --git a/src/app/cartography/shared/datasources/symbols-datasource.spec.ts b/src/app/cartography/shared/datasources/symbols-datasource.spec.ts new file mode 100644 index 00000000..84deecaa --- /dev/null +++ b/src/app/cartography/shared/datasources/symbols-datasource.spec.ts @@ -0,0 +1,33 @@ +import {SymbolsDataSource} from "./symbols-datasource"; +import {Symbol} from "../models/symbol"; + + +describe('SymbolsDataSource', () => { + let dataSource: SymbolsDataSource; + let data: Symbol[]; + + beforeEach(() => { + dataSource = new SymbolsDataSource(); + dataSource.connect().subscribe((symbols: Symbol[]) => { + data = symbols; + }); + }); + + describe('Symbol can be updated', () => { + beforeEach(() => { + const symbol = new Symbol(); + symbol.symbol_id = "1"; + symbol.filename = "test-1"; + dataSource.add(symbol); + + symbol.filename = "test-2"; + dataSource.update(symbol); + }); + + it('filename should change', () => { + expect(data[0].symbol_id).toEqual("1"); + expect(data[0].filename).toEqual("test-2"); + }); + }); + +}); diff --git a/src/app/cartography/shared/datasources/symbols-datasource.ts b/src/app/cartography/shared/datasources/symbols-datasource.ts new file mode 100644 index 00000000..b08b36ca --- /dev/null +++ b/src/app/cartography/shared/datasources/symbols-datasource.ts @@ -0,0 +1,12 @@ +import {Node} from "../models/node"; +import {DataSource} from "./datasource"; +import {Injectable} from "@angular/core"; +import {Symbol} from "../models/symbol"; + + +@Injectable() +export class SymbolsDataSource extends DataSource { + protected findIndex(symbol: Symbol) { + return this.data.findIndex((s: Symbol) => s.symbol_id === symbol.symbol_id); + } +} diff --git a/src/app/cartography/shared/managers/selection-manager.spec.ts b/src/app/cartography/shared/managers/selection-manager.spec.ts new file mode 100644 index 00000000..9afee554 --- /dev/null +++ b/src/app/cartography/shared/managers/selection-manager.spec.ts @@ -0,0 +1,76 @@ +import { Subject} from "rxjs/Subject"; + +import { Node } from "../models/node"; +import { Link } from "../models/link"; +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 "../../map/helpers/in-rectangle-helper"; + + +describe('SelectionManager', () => { + let manager: SelectionManager; + let selectedRectangleSubject: Subject; + let nodesDataSource: NodesDataSource; + + beforeEach(() => { + const linksDataSource = new LinksDataSource(); + const inRectangleHelper = new InRectangleHelper(); + + selectedRectangleSubject = new Subject(); + + nodesDataSource = new NodesDataSource(); + + manager = new SelectionManager(nodesDataSource, linksDataSource, inRectangleHelper); + manager.subscribe(selectedRectangleSubject); + + const node_1 = new Node(); + node_1.node_id = "test1"; + node_1.name = "Node 1"; + node_1.x = 150; + node_1.y = 150; + + nodesDataSource.add(node_1); + + const node_2 = new Node(); + node_2.node_id = "test2"; + node_2.name = "Node 2"; + node_2.x = 300; + node_2.y = 300; + nodesDataSource.add(node_2); + + const link_1 = new Link(); + link_1.link_id = "test1"; + linksDataSource.add(link_1); + }); + + it('node should be selected', () => { + selectedRectangleSubject.next(new Rectangle(100, 100, 100, 100)); + expect(nodesDataSource.getItems()[0].is_selected).toEqual(true); + expect(manager.getSelectedNodes().length).toEqual(1); + expect(manager.getSelectedLinks().length).toEqual(0); + }); + + it('node should be selected and deselected', () => { + selectedRectangleSubject.next(new Rectangle(100, 100, 100, 100)); + selectedRectangleSubject.next(new Rectangle(350, 350, 100, 100)); + expect(nodesDataSource.getItems()[0].is_selected).toEqual(false); + expect(manager.getSelectedNodes().length).toEqual(0); + expect(manager.getSelectedLinks().length).toEqual(0); + }); + + it('nodes should be manually selected', () => { + const node = new Node(); + node.node_id = "test1"; + manager.setSelectedNodes([node]); + expect(manager.getSelectedNodes().length).toEqual(1); + }); + + it('links should be manually selected', () => { + const link = new Link(); + link.link_id = "test1"; + manager.setSelectedLinks([link]); + expect(manager.getSelectedLinks().length).toEqual(1); + }); +}); diff --git a/src/app/cartography/shared/managers/selection-manager.ts b/src/app/cartography/shared/managers/selection-manager.ts new file mode 100644 index 00000000..965f058d --- /dev/null +++ b/src/app/cartography/shared/managers/selection-manager.ts @@ -0,0 +1,84 @@ +import { Injectable } from "@angular/core"; + +import { Subject } from "rxjs/Subject"; +import { Subscription } from "rxjs/Subscription"; + +import { NodesDataSource } from "../datasources/nodes-datasource"; +import { LinksDataSource } from "../datasources/links-datasource"; +import { Node } from "../models/node"; +import { InRectangleHelper } from "../../map/helpers/in-rectangle-helper"; +import { Rectangle } from "../models/rectangle"; +import { Link} from "../models/link"; +import { DataSource } from "../datasources/datasource"; + + +export interface Selectable { + x: number; + y: number; + is_selected: boolean; +} + +@Injectable() +export class SelectionManager { + private selectedNodes: Node[] = []; + private selectedLinks: Link[] = []; + private subscription: Subscription; + + constructor(private nodesDataSource: NodesDataSource, + private linksDataSource: LinksDataSource, + private inRectangleHelper: InRectangleHelper) {} + + + public subscribe(subject: Subject) { + this.subscription = subject.subscribe((rectangle: Rectangle) => { + this.selectedNodes = this.getSelectedItemsInRectangle(this.nodesDataSource, rectangle); + this.selectedLinks = this.getSelectedItemsInRectangle(this.linksDataSource, rectangle); + }); + } + + public getSelectedNodes() { + return this.selectedNodes; + } + + public getSelectedLinks() { + return this.selectedLinks; + } + + public setSelectedNodes(nodes: Node[]) { + this.selectedNodes = this.setSelectedItems(this.nodesDataSource, (node: Node) => { + return !!nodes.find((n: Node) => node.node_id === n.node_id); + }); + } + + public setSelectedLinks(links: Link[]) { + this.selectedLinks = this.setSelectedItems(this.linksDataSource, (link: Link) => { + return !!links.find((l: Link) => link.link_id === l.link_id); + }); + } + + private getSelectedItemsInRectangle(dataSource: DataSource, rectangle: Rectangle) { + return this.setSelectedItems(dataSource, (item: T) => { + return this.inRectangleHelper.inRectangle(item, rectangle); + }); + } + + private setSelected(item: T, isSelected: boolean, dataSource: DataSource): boolean { + if (item.is_selected !== isSelected) { + item.is_selected = isSelected; + dataSource.update(item); + } + return item.is_selected; + } + + private setSelectedItems(dataSource: DataSource, discriminator: (item: T) => boolean) { + const selected: T[] = []; + dataSource.getItems().forEach((item: T) => { + const isSelected = discriminator(item); + this.setSelected(item, isSelected, dataSource); + if (isSelected) { + selected.push(item); + } + }); + return selected; + } +} diff --git a/src/app/cartography/shared/models/context.ts b/src/app/cartography/shared/models/context.ts new file mode 100644 index 00000000..2399beba --- /dev/null +++ b/src/app/cartography/shared/models/context.ts @@ -0,0 +1,26 @@ +import {Size} from "./size"; +import {Point} from "./point"; + + +export class Context { + private size: Size; + + constructor(private centerZeroZeroPoint = false) { + this.size = new Size(0, 0); + } + + public getSize(): Size { + return this.size; + } + + public setSize(size: Size): void { + this.size = size; + } + + public getZeroZeroTransformationPoint() { + if (this.centerZeroZeroPoint) { + return new Point(this.getSize().width / 2., this.getSize().height / 2.); + } + return new Point(0, 0); + } +} diff --git a/src/app/cartography/shared/models/drawing-line.model.ts b/src/app/cartography/shared/models/drawing-line.ts similarity index 61% rename from src/app/cartography/shared/models/drawing-line.model.ts rename to src/app/cartography/shared/models/drawing-line.ts index 764100e9..57c03631 100644 --- a/src/app/cartography/shared/models/drawing-line.model.ts +++ b/src/app/cartography/shared/models/drawing-line.ts @@ -1,4 +1,4 @@ -import {Point} from "./point.model"; +import {Point} from "./point"; export class DrawingLine { start: Point; diff --git a/src/app/cartography/shared/models/drawing.model.ts b/src/app/cartography/shared/models/drawing.ts similarity index 100% rename from src/app/cartography/shared/models/drawing.model.ts rename to src/app/cartography/shared/models/drawing.ts diff --git a/src/app/cartography/shared/models/label.model.ts b/src/app/cartography/shared/models/label.ts similarity index 100% rename from src/app/cartography/shared/models/label.model.ts rename to src/app/cartography/shared/models/label.ts diff --git a/src/app/cartography/shared/models/link-status.model.ts b/src/app/cartography/shared/models/link-status.ts similarity index 100% rename from src/app/cartography/shared/models/link-status.model.ts rename to src/app/cartography/shared/models/link-status.ts diff --git a/src/app/cartography/shared/models/link.model.ts b/src/app/cartography/shared/models/link.ts similarity index 65% rename from src/app/cartography/shared/models/link.model.ts rename to src/app/cartography/shared/models/link.ts index a87c697c..7a3cdb10 100644 --- a/src/app/cartography/shared/models/link.model.ts +++ b/src/app/cartography/shared/models/link.ts @@ -1,6 +1,7 @@ -import {Node} from "./node.model"; +import {Node} from "./node"; +import {Selectable} from "../managers/selection-manager"; -export class Link { +export class Link implements Selectable { capture_file_name: string; capture_file_path: string; capturing: boolean; @@ -12,4 +13,8 @@ export class Link { length: number; // this is not from server source: Node; // this is not from server target: Node; // this is not from server + + is_selected = false; + x: number; + y: number; } diff --git a/src/app/cartography/shared/models/node.model.ts b/src/app/cartography/shared/models/node.ts similarity index 76% rename from src/app/cartography/shared/models/node.model.ts rename to src/app/cartography/shared/models/node.ts index ce68d27a..9572985c 100644 --- a/src/app/cartography/shared/models/node.model.ts +++ b/src/app/cartography/shared/models/node.ts @@ -1,7 +1,8 @@ -import {Label} from "./label.model"; +import {Label} from "./label"; import {Port} from "../../../shared/models/port"; +import {Selectable} from "../managers/selection-manager"; -export class Node { +export class Node implements Selectable { command_line: string; compute_id: string; console: number; @@ -24,4 +25,5 @@ export class Node { x: number; y: number; z: number; + is_selected = false; } diff --git a/src/app/cartography/shared/models/point.model.ts b/src/app/cartography/shared/models/point.ts similarity index 100% rename from src/app/cartography/shared/models/point.model.ts rename to src/app/cartography/shared/models/point.ts diff --git a/src/app/cartography/shared/models/rectangle.ts b/src/app/cartography/shared/models/rectangle.ts new file mode 100644 index 00000000..1fb5349a --- /dev/null +++ b/src/app/cartography/shared/models/rectangle.ts @@ -0,0 +1,8 @@ +export class Rectangle { + constructor( + public x?: number, + public y?: number, + public width?: number, + public height?: number + ) {} +} diff --git a/src/app/cartography/shared/models/size.model.ts b/src/app/cartography/shared/models/size.ts similarity index 100% rename from src/app/cartography/shared/models/size.model.ts rename to src/app/cartography/shared/models/size.ts diff --git a/src/app/shared/models/symbol.ts b/src/app/cartography/shared/models/symbol.ts similarity index 100% rename from src/app/shared/models/symbol.ts rename to src/app/cartography/shared/models/symbol.ts diff --git a/src/app/map/models/types.ts b/src/app/cartography/shared/models/types.ts similarity index 100% rename from src/app/map/models/types.ts rename to src/app/cartography/shared/models/types.ts diff --git a/src/app/cartography/shared/tool.ts b/src/app/cartography/shared/tool.ts new file mode 100644 index 00000000..714fb2c7 --- /dev/null +++ b/src/app/cartography/shared/tool.ts @@ -0,0 +1,7 @@ +import {SVGSelection} from "./models/types"; + +export interface Tool { + connect(selection: SVGSelection); + activate(); + deactivate(); +} diff --git a/src/app/cartography/shared/tools/moving-tool.spec.ts b/src/app/cartography/shared/tools/moving-tool.spec.ts new file mode 100644 index 00000000..d8333f41 --- /dev/null +++ b/src/app/cartography/shared/tools/moving-tool.spec.ts @@ -0,0 +1,75 @@ +import { select } from "d3-selection"; +import { Context } from "../models/context"; +import { SVGSelection } from "../models/types"; +import { MovingTool } from "./moving-tool"; + + +describe('MovingTool', () => { + let tool: MovingTool; + let svg: SVGSelection; + let context: Context; + let node: SVGSelection; + let canvas: SVGSelection; + + beforeEach(() => { + tool = new MovingTool(); + + svg = select('body') + .append('svg') + .attr('width', 1000) + .attr('height', 1000); + + canvas = svg.append('g').attr('class', 'canvas'); + + node = canvas + .append('g') + .attr('class', 'node') + .attr('x', 10) + .attr('y', 20); + + + context = new Context(); + + tool.connect(svg, context); + tool.draw(svg, context); + tool.activate(); + + }); + + describe('MovingTool can move canvas', () => { + beforeEach(() => { + svg.node().dispatchEvent( + new MouseEvent('mousedown', { + clientX: 100, clientY: 100, relatedTarget: svg.node(), + screenY: 1024, screenX: 1024, view: window + }) + ); + + window.dispatchEvent(new MouseEvent('mousemove', {clientX: 200, clientY: 200})); + window.dispatchEvent(new MouseEvent('mouseup', {clientX: 200, clientY: 200, view: window})); + }); + + it('canvas should transformed', () => { + expect(canvas.attr('transform')).toEqual('translate(100, 100) scale(1)'); + }); + }); + + describe('MovingTool can be deactivated', () => { + beforeEach(() => { + tool.deactivate(); + + svg.node().dispatchEvent( + new MouseEvent('mousedown', { + clientX: 100, clientY: 100, relatedTarget: svg.node(), + screenY: 1024, screenX: 1024, view: window + }) + ); + + window.dispatchEvent(new MouseEvent('mousemove', {clientX: 200, clientY: 200})); + }); + + it('canvas cannot be transformed', () => { + expect(canvas.attr('transform')).toBeNull(); + }); + }); +}); diff --git a/src/app/cartography/shared/tools/moving-tool.ts b/src/app/cartography/shared/tools/moving-tool.ts new file mode 100644 index 00000000..f954b9f0 --- /dev/null +++ b/src/app/cartography/shared/tools/moving-tool.ts @@ -0,0 +1,49 @@ +import {SVGSelection} from "../models/types"; +import {Context} from "../models/context"; +import {D3ZoomEvent, zoom, ZoomBehavior} from "d3-zoom"; +import { event } from "d3-selection"; + +export class MovingTool { + private selection: SVGSelection; + private context: Context; + private zoom: ZoomBehavior; + + constructor() { + this.zoom = zoom() + .scaleExtent([1 / 2, 8]); + } + + public connect(selection: SVGSelection, context: Context) { + this.selection = selection; + this.context = context; + + } + + public draw(selection: SVGSelection, context: Context) { + this.selection = selection; + this.context = context; + } + + public activate() { + const self = this; + + const onZoom = function(this: SVGSVGElement) { + + const canvas = self.selection.select("g.canvas"); + const e: D3ZoomEvent = event; + canvas.attr( + 'transform', + `translate(${self.context.getSize().width / 2 + e.transform.x}, ` + + `${self.context.getSize().height / 2 + e.transform.y}) scale(${e.transform.k})`); + }; + + this.zoom.on('zoom', onZoom); + this.selection.call(this.zoom); + } + + public deactivate() { + // d3.js preserves event `mousedown.zoom` and blocks selection + this.selection.on('mousedown.zoom', null); + this.zoom.on('zoom', null); + } +} diff --git a/src/app/cartography/shared/tools/selection-tool.spec.ts b/src/app/cartography/shared/tools/selection-tool.spec.ts new file mode 100644 index 00000000..b5b8bc7f --- /dev/null +++ b/src/app/cartography/shared/tools/selection-tool.spec.ts @@ -0,0 +1,120 @@ +import { select } from "d3-selection"; + +import { SelectionTool } from "./selection-tool"; +import { Context } from "../models/context"; +import { SVGSelection } from "../models/types"; +import { Rectangle } from "../models/rectangle"; + + +describe('SelectionTool', () => { + let tool: SelectionTool; + let svg: SVGSelection; + let context: Context; + let selection_line_tool: SVGSelection; + let path_selection: SVGSelection; + let selected_rectangle: Rectangle; + + beforeEach(() => { + tool = new SelectionTool(); + + tool.rectangleSelected.subscribe((rectangle: Rectangle) => { + selected_rectangle = rectangle; + }); + + svg = select('body') + .append('svg') + .attr('width', 1000) + .attr('height', 1000); + + svg.append('g').attr('class', 'canvas'); + + context = new Context(); + + tool.connect(svg, context); + tool.draw(svg, context); + tool.activate(); + + selection_line_tool = svg.select('g.selection-line-tool'); + path_selection = selection_line_tool.select('path.selection'); + }); + + it('creates selection-line-tool container with path', () => { + expect(selection_line_tool.node()).not.toBeNull(); + expect(selection_line_tool.select('path')).not.toBeNull(); + expect(path_selection.attr('visibility')).toEqual('hidden'); + }); + + describe('SelectionTool can handle start of selection', () => { + beforeEach(() => { + svg.node().dispatchEvent(new MouseEvent('mousedown', {clientX: 100, clientY: 100})); + }); + + it('path should be visible and have parameters', () => { + expect(path_selection.attr('visibility')).toEqual('visible'); + expect(path_selection.attr('d')).toEqual('M95,86 l0,0 l0,0 l0,0z'); + }); + }); + + describe('SelectionTool can handle move of selection', () => { + beforeEach(() => { + svg.node().dispatchEvent(new MouseEvent('mousedown', {clientX: 100, clientY: 100})); + window.dispatchEvent(new MouseEvent('mousemove', {clientX: 300, clientY: 300})); + }); + + it('path should have got changed parameters', () => { + expect(path_selection.attr('d')).toEqual('M95,86 l200,0 l0,200 l-200,0z'); + }); + }); + + describe('SelectionTool can handle end of selection', () => { + beforeEach(() => { + svg.node().dispatchEvent(new MouseEvent('mousedown', {clientX: 100, clientY: 100})); + window.dispatchEvent(new MouseEvent('mousemove', {clientX: 200, clientY: 200})); + window.dispatchEvent(new MouseEvent('mouseup', {clientX: 200, clientY: 200})); + }); + + it('path should be hidden', () => { + expect(path_selection.attr('visibility')).toEqual('hidden'); + }); + + it('rectangle should be selected', () => { + expect(selected_rectangle).toEqual(new Rectangle(95, 86, 100, 100)); + }); + + describe('SelectionTool can deselect after click outside', () => { + beforeEach(() => { + svg.node().dispatchEvent(new MouseEvent('mousedown', {clientX: 300, clientY: 300})); + window.dispatchEvent(new MouseEvent('mouseup', {clientX: 300, clientY: 300})); + }); + + it('rectangle should be selected', () => { + expect(selected_rectangle).toEqual(new Rectangle(295, 286, 0, 0)); + }); + }); + }); + + describe('SelectionTool can handle end of selection in reverse direction', () => { + beforeEach(() => { + svg.node().dispatchEvent(new MouseEvent('mousedown', {clientX: 200, clientY: 200})); + window.dispatchEvent(new MouseEvent('mousemove', {clientX: 100, clientY: 100})); + window.dispatchEvent(new MouseEvent('mouseup', {clientX: 100, clientY: 100})); + }); + + it('rectangle should be selected', () => { + expect(selected_rectangle).toEqual(new Rectangle(95, 86, 100, 100)); + }); + }); + + describe('SelectionTool can be deactivated', () => { + beforeEach(() => { + tool.deactivate(); + svg.node().dispatchEvent(new MouseEvent('mousedown', {clientX: 100, clientY: 100})); + }); + + it('path should be still hiden', () => { + expect(path_selection.attr('visibility')).toEqual('hidden'); + }); + }); + + +}); diff --git a/src/app/cartography/shared/tools/selection-tool.ts b/src/app/cartography/shared/tools/selection-tool.ts new file mode 100644 index 00000000..f7bc4f6e --- /dev/null +++ b/src/app/cartography/shared/tools/selection-tool.ts @@ -0,0 +1,112 @@ +import { Injectable } from "@angular/core"; +import { mouse, select } from "d3-selection"; +import { Subject } from "rxjs/Subject"; + +import { SVGSelection } from "../models/types"; +import { Context } from "../models/context"; +import { Rectangle } from "../models/rectangle"; + + +@Injectable() +export class SelectionTool { + static readonly SELECTABLE_CLASS = '.selectable'; + + public rectangleSelected: Subject; + + private selection: SVGSelection; + private path; + private context: Context; + + public constructor() { + this.rectangleSelected = new Subject(); + } + + public connect(selection: SVGSelection, context: Context) { + this.selection = selection; + this.context = context; + } + + public activate() { + const self = this; + + + this.selection.on("mousedown", function() { + const subject = select(window); + const parent = this.parentElement; + + const start = self.transformation(mouse(parent)); + self.startSelection(start); + + // clear selection + self.selection + .selectAll(SelectionTool.SELECTABLE_CLASS) + .classed("selected", false); + + subject + .on("mousemove.selection", function() { + const end = self.transformation(mouse(parent)); + self.moveSelection(start, end); + }).on("mouseup.selection", function() { + const end = self.transformation(mouse(parent)); + self.endSelection(start, end); + subject + .on("mousemove.selection", null) + .on("mouseup.selection", null); + }); + }); + } + + public deactivate() { + this.selection.on('mousedown', null); + } + + public draw(selection: SVGSelection, context: Context) { + const canvas = selection.select("g.canvas"); + + if (!canvas.select("g.selection-line-tool").node()) { + const g = canvas.append('g'); + g.attr("class", "selection-line-tool"); + + this.path = g.append("path"); + this.path + .attr("class", "selection") + .attr("visibility", "hidden"); + } + this.selection = selection; + } + + private startSelection(start) { + this.path + .attr("d", this.rect(start[0], start[1], 0, 0)) + .attr("visibility", "visible"); + } + + private moveSelection(start, move) { + this.path.attr("d", this.rect(start[0], start[1], move[0] - start[0], move[1] - start[1])); + this.selectedEvent(start, move); + } + + private endSelection(start, end) { + this.path.attr("visibility", "hidden"); + this.selectedEvent(start, end); + } + + private selectedEvent(start, end) { + const x = Math.min(start[0], end[0]); + const y = Math.min(start[1], end[1]); + const width = Math.abs(start[0] - end[0]); + const height = Math.abs(start[1] - end[1]); + this.rectangleSelected.next(new Rectangle(x, y, width, height)); + } + + private rect(x: number, y: number, w: number, h: number) { + return "M" + [x, y] + " l" + [w, 0] + " l" + [0, h] + " l" + [-w, 0] + "z"; + } + + private transformation(point) { + const transformation_point = this.context.getZeroZeroTransformationPoint(); + return [point[0] - transformation_point.x, point[1] - transformation_point.y]; + } + + +} diff --git a/src/app/cartography/shared/widgets/drawing-line.widget.ts b/src/app/cartography/shared/widgets/drawing-line.widget.ts index 0904a219..eb5a1ae6 100644 --- a/src/app/cartography/shared/widgets/drawing-line.widget.ts +++ b/src/app/cartography/shared/widgets/drawing-line.widget.ts @@ -1,8 +1,9 @@ -import {DrawingLine} from "../models/drawing-line.model"; -import {SVGSelection} from "../../../map/models/types"; -import {Point} from "../models/point.model"; +import {DrawingLine} from "../models/drawing-line"; +import {SVGSelection} from "../models/types"; +import {Point} from "../models/point"; import {line} from "d3-shape"; -import {event, mouse, select} from "d3-selection"; +import {mouse} from "d3-selection"; +import {Context} from "../models/context"; export class DrawingLineWidget { private drawingLine: DrawingLine = new DrawingLine(); @@ -23,11 +24,11 @@ export class DrawingLineWidget { const coordinates = mouse(node); self.drawingLine.end.x = coordinates[0]; self.drawingLine.end.y = coordinates[1]; - self.draw(); + self.draw(null, null); }; this.selection.on('mousemove', over); - this.draw(); + this.draw(null, null); } public isDrawing() { @@ -37,19 +38,20 @@ export class DrawingLineWidget { public stop() { this.drawing = false; this.selection.on('mousemove', null); - this.draw(); + this.draw(null, null); return this.data; } - public connect(selection: SVGSelection) { + public connect(selection: SVGSelection, context: Context) { this.selection = selection; + } + + public draw(selection: SVGSelection, context: Context) { const canvas = this.selection.select("g.canvas"); if (!canvas.select("g.drawing-line-tool").node()) { canvas.append('g').attr("class", "drawing-line-tool"); } - } - public draw() { let link_data = []; if (this.drawing) { diff --git a/src/app/cartography/shared/widgets/drawings.widget.ts b/src/app/cartography/shared/widgets/drawings.widget.ts index 621b0c8c..b6eedd34 100644 --- a/src/app/cartography/shared/widgets/drawings.widget.ts +++ b/src/app/cartography/shared/widgets/drawings.widget.ts @@ -1,6 +1,6 @@ import {Widget} from "./widget"; -import {Drawing} from "../models/drawing.model"; -import {SVGSelection} from "../../../map/models/types"; +import {Drawing} from "../models/drawing"; +import {SVGSelection} from "../models/types"; export class DrawingsWidget implements Widget { diff --git a/src/app/cartography/shared/widgets/ethernet-link.widget.ts b/src/app/cartography/shared/widgets/ethernet-link.widget.ts index 69240a10..926af96e 100644 --- a/src/app/cartography/shared/widgets/ethernet-link.widget.ts +++ b/src/app/cartography/shared/widgets/ethernet-link.widget.ts @@ -1,8 +1,8 @@ import {Widget} from "./widget"; -import {SVGSelection} from "../../../map/models/types"; +import {SVGSelection} from "../models/types"; import { line } from "d3-shape"; -import {Link} from "../models/link.model"; +import {Link} from "../models/link"; export class EthernetLinkWidget implements Widget { @@ -15,6 +15,7 @@ export class EthernetLinkWidget implements Widget { const value_line = line(); let link_path = view.select('path'); + link_path.classed('selected', (l: Link) => l.is_selected); if (!link_path.node()) { link_path = view.append('path'); diff --git a/src/app/cartography/shared/widgets/graph.widget.ts b/src/app/cartography/shared/widgets/graph.widget.ts index fa4de8f3..9cada770 100644 --- a/src/app/cartography/shared/widgets/graph.widget.ts +++ b/src/app/cartography/shared/widgets/graph.widget.ts @@ -1,15 +1,15 @@ -import { Context } from "../../../map/models/context"; -import { Node } from "../models/node.model"; -import { Link } from "../models/link.model"; +import { Context } from "../models/context"; +import { Node } from "../models/node"; +import { Link } from "../models/link"; import { NodesWidget } from "./nodes.widget"; import { Widget } from "./widget"; -import { SVGSelection } from "../../../map/models/types"; +import { SVGSelection } from "../models/types"; import { LinksWidget } from "./links.widget"; -import { D3ZoomEvent, zoom } from "d3-zoom"; -import { event } from "d3-selection"; -import { Drawing } from "../models/drawing.model"; +import { Drawing } from "../models/drawing"; import { DrawingsWidget } from "./drawings.widget"; import { DrawingLineWidget } from "./drawing-line.widget"; +import {SelectionTool} from "../tools/selection-tool"; +import {MovingTool} from "../tools/moving-tool"; export class GraphLayout implements Widget { private nodes: Node[] = []; @@ -20,6 +20,8 @@ export class GraphLayout implements Widget { private nodesWidget: NodesWidget; private drawingsWidget: DrawingsWidget; private drawingLineTool: DrawingLineWidget; + private selectionTool: SelectionTool; + private movingTool: MovingTool; private centerZeroZeroPoint = true; @@ -28,6 +30,8 @@ export class GraphLayout implements Widget { this.nodesWidget = new NodesWidget(); this.drawingsWidget = new DrawingsWidget(); this.drawingLineTool = new DrawingLineWidget(); + this.selectionTool = new SelectionTool(); + this.movingTool = new MovingTool(); } public setNodes(nodes: Node[]) { @@ -54,9 +58,23 @@ export class GraphLayout implements Widget { return this.drawingLineTool; } - draw(view: SVGSelection, context: Context) { - const self = this; + 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) { const canvas = view .selectAll('g.canvas') .data([context]); @@ -71,29 +89,18 @@ export class GraphLayout implements Widget { (ctx: Context) => `translate(${ctx.getSize().width / 2}, ${ctx.getSize().height / 2})`); } - - this.linksWidget.draw(canvas, this.links); this.nodesWidget.draw(canvas, this.nodes); this.drawingsWidget.draw(canvas, this.drawings); - this.drawingLineTool.connect(view); - - const onZoom = function(this: SVGSVGElement) { - const e: D3ZoomEvent = event; - if (self.centerZeroZeroPoint) { - canvas.attr( - 'transform', - `translate(${context.getSize().width / 2 + e.transform.x}, ` + - `${context.getSize().height / 2 + e.transform.y}) scale(${e.transform.k})`); - } else { - canvas.attr('transform', e.transform.toString()); - } - }; - - view.call(zoom() - .scaleExtent([1 / 2, 8]) - .on('zoom', onZoom)); + this.drawingLineTool.draw(view, context); + this.selectionTool.draw(view, context); + this.movingTool.draw(view, context); } + disconnect(view: SVGSelection) { + if (view.empty && !view.empty()) { + view.selectAll('*').remove(); + } + } } diff --git a/src/app/cartography/shared/widgets/links.widget.ts b/src/app/cartography/shared/widgets/links.widget.ts index 4d003169..a989ec04 100644 --- a/src/app/cartography/shared/widgets/links.widget.ts +++ b/src/app/cartography/shared/widgets/links.widget.ts @@ -1,9 +1,9 @@ import {BaseType, select, Selection} from "d3-selection"; import { Widget } from "./widget"; -import { SVGSelection } from "../../../map/models/types"; -import { Link } from "../models/link.model"; -import { LinkStatus } from "../models/link-status.model"; +import { SVGSelection } from "../models/types"; +import { Link } from "../models/link"; +import { LinkStatus } from "../models/link-status"; import { MultiLinkCalculatorHelper } from "../../map/helpers/multi-link-calculator-helper"; import {SerialLinkWidget} from "./serial-link.widget"; import {EthernetLinkWidget} from "./ethernet-link.widget"; diff --git a/src/app/cartography/shared/widgets/nodes.widget.ts b/src/app/cartography/shared/widgets/nodes.widget.ts index 1332a21e..8e456804 100644 --- a/src/app/cartography/shared/widgets/nodes.widget.ts +++ b/src/app/cartography/shared/widgets/nodes.widget.ts @@ -1,9 +1,9 @@ import { Widget } from "./widget"; -import { Node } from "../models/node.model"; -import { SVGSelection } from "../../../map/models/types"; +import { Node } from "../models/node"; +import { SVGSelection } from "../models/types"; import {event, select} from "d3-selection"; import {D3DragEvent, drag} from "d3-drag"; -import {Symbol} from "../../../shared/models/symbol"; +import {Symbol} from "../models/symbol"; export class NodesWidget implements Widget { @@ -136,6 +136,7 @@ export class NodesWidget implements Widget { const node_merge = node .merge(node_enter) + .classed('selected', (n: Node) => n.is_selected) .on("contextmenu", function (n: Node, i: number) { event.preventDefault(); if (self.onContextMenuCallback !== null) { diff --git a/src/app/cartography/shared/widgets/serial-link.widget.ts b/src/app/cartography/shared/widgets/serial-link.widget.ts index 325b1dfd..7865acec 100644 --- a/src/app/cartography/shared/widgets/serial-link.widget.ts +++ b/src/app/cartography/shared/widgets/serial-link.widget.ts @@ -1,6 +1,6 @@ import {Widget} from "./widget"; -import {SVGSelection} from "../../../map/models/types"; -import {Link} from "../models/link.model"; +import {SVGSelection} from "../models/types"; +import {Link} from "../models/link"; import { path } from "d3-path"; diff --git a/src/app/map/models/context.ts b/src/app/map/models/context.ts deleted file mode 100644 index da853e1f..00000000 --- a/src/app/map/models/context.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {Size} from "../../cartography/shared/models/size.model"; -import {Selection} from "d3-selection"; - -export class Context { - private size: Size; - private root: Selection; - - constructor(root: Selection) { - this.root = root; - } - - public getSize(): Size { - return this.size; - } - - public setSize(size: Size): void { - this.size = size; - } -} diff --git a/src/app/project-map/project-map.component.css b/src/app/project-map/project-map.component.css index 0574f1db..0fb72dce 100644 --- a/src/app/project-map/project-map.component.css +++ b/src/app/project-map/project-map.component.css @@ -43,6 +43,37 @@ g.node text { } -svg image:hover, svg image.chosen { +svg image:hover, svg image.chosen, g.selected { filter: grayscale(100%); } + +path.selected { + stroke: darkred; +} + +.selection-line-tool .selection { + fill: #7ccbe1; + stroke: #66aec2 ; + fill-opacity: 0.3; + stroke-opacity: 0.7; + stroke-width: 1; + stroke-dasharray: 5, 5; +} + + +g.node text, +.noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* Disable outline after button click */ +.project-toolbar button { + outline: 0; + border: none; + -moz-outline-style: none +} diff --git a/src/app/project-map/project-map.component.html b/src/app/project-map/project-map.component.html index f47bbbf5..66a8fdea 100644 --- a/src/app/project-map/project-map.component.html +++ b/src/app/project-map/project-map.component.html @@ -9,7 +9,7 @@ - + + - diff --git a/src/app/project-map/project-map.component.ts b/src/app/project-map/project-map.component.ts index d2a4811a..78e4236a 100644 --- a/src/app/project-map/project-map.component.ts +++ b/src/app/project-map/project-map.component.ts @@ -12,27 +12,33 @@ import 'rxjs/add/observable/dom/webSocket'; import { Project } from '../shared/models/project'; -import { Node } from '../cartography/shared/models/node.model'; +import { Node } from '../cartography/shared/models/node'; import { SymbolService } from '../shared/services/symbol.service'; -import { Link } from "../cartography/shared/models/link.model"; +import { Link } from "../cartography/shared/models/link"; import { MapComponent } from "../cartography/map/map.component"; import { ServerService } from "../shared/services/server.service"; import { ProjectService } from '../shared/services/project.service'; import { Server } from "../shared/models/server"; -import { MAT_DIALOG_DATA, MatDialog, MatDialogRef, MatSnackBar } from "@angular/material"; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material"; import { SnapshotService } from "../shared/services/snapshot.service"; import { Snapshot } from "../shared/models/snapshot"; import { ProgressDialogService } from "../shared/progress-dialog/progress-dialog.service"; import { ProgressDialogComponent } from "../shared/progress-dialog/progress-dialog.component"; -import { Drawing } from "../cartography/shared/models/drawing.model"; +import { Drawing } from "../cartography/shared/models/drawing"; import { NodeContextMenuComponent } from "../shared/node-context-menu/node-context-menu.component"; import { Appliance } from "../shared/models/appliance"; import { NodeService } from "../shared/services/node.service"; -import { Symbol } from "../shared/models/symbol"; +import { Symbol } from "../cartography/shared/models/symbol"; import { NodeSelectInterfaceComponent } from "../shared/node-select-interface/node-select-interface.component"; import { Port } from "../shared/models/port"; import { LinkService } from "../shared/services/link.service"; import { ToasterService } from '../shared/services/toaster.service'; +import { NodesDataSource } from "../cartography/shared/datasources/nodes-datasource"; +import { LinksDataSource } from "../cartography/shared/datasources/links-datasource"; +import { ProjectWebServiceHandler } from "../shared/handlers/project-web-service-handler"; +import { Rectangle } from "../cartography/shared/models/rectangle"; +import { SelectionManager } from "../cartography/shared/managers/selection-manager"; +import { InRectangleHelper } from "../cartography/map/helpers/in-rectangle-helper"; @Component({ @@ -52,6 +58,8 @@ export class ProjectMapComponent implements OnInit { private ws: Subject; private drawLineMode = false; + private movingMode = false; + public isLoading = true; @ViewChild(MapComponent) mapChild: MapComponent; @@ -69,7 +77,11 @@ export class ProjectMapComponent implements OnInit { private linkService: LinkService, private dialog: MatDialog, private progressDialogService: ProgressDialogService, - private toaster: ToasterService) { + private toaster: ToasterService, + private projectWebServiceHandler: ProjectWebServiceHandler, + protected nodesDataSource: NodesDataSource, + protected linksDataSource: LinksDataSource, + ) { } ngOnInit() { @@ -102,6 +114,19 @@ export class ProjectMapComponent implements OnInit { this.symbols = symbols; }); + this.nodesDataSource.connect().subscribe((nodes: Node[]) => { + this.nodes = nodes; + if (this.mapChild) { + this.mapChild.reload(); + } + }); + + this.linksDataSource.connect().subscribe((links: Link[]) => { + this.links = links; + if (this.mapChild) { + this.mapChild.reload(); + } + }); } onProjectLoad(project: Project) { @@ -115,11 +140,11 @@ export class ProjectMapComponent implements OnInit { return this.projectService.links(this.server, project.project_id); }) .flatMap((links: Link[]) => { - this.links = links; + this.linksDataSource.set(links); return this.projectService.nodes(this.server, project.project_id); }) .subscribe((nodes: Node[]) => { - this.nodes = nodes; + this.nodesDataSource.set(nodes); this.setUpMapCallbacks(project); this.setUpWS(project); @@ -132,85 +157,33 @@ export class ProjectMapComponent implements OnInit { setUpWS(project: Project) { this.ws = Observable.webSocket( this.projectService.notificationsPath(this.server, project.project_id)); - - this.ws.subscribe((o: any) => { - if (o.action === 'node.updated') { - const node: Node = o.event; - const index = this.nodes.findIndex((n: Node) => n.node_id === node.node_id); - if (index >= 0) { - this.nodes[index] = node; - this.mapChild.reload(); // temporary invocation - } - } - if (o.action === 'node.created') { - const node: Node = o.event; - const index = this.nodes.findIndex((n: Node) => n.node_id === node.node_id); - if (index === -1) { - this.nodes.push(node); - this.mapChild.reload(); // temporary invocation - } - } - if (o.action === 'node.deleted') { - const node: Node = o.event; - const index = this.nodes.findIndex((n: Node) => n.node_id === node.node_id); - if (index >= 0) { - this.nodes.splice(index, 1); - this.mapChild.reload(); // temporary invocation - } - } - if (o.action === 'link.created') { - const link: Link = o.event; - const index = this.links.findIndex((l: Link) => l.link_id === link.link_id); - if (index === -1) { - this.links.push(link); - this.mapChild.reload(); // temporary invocation - } - } - if (o.action === 'link.updated') { - const link: Link = o.event; - const index = this.links.findIndex((l: Link) => l.link_id === link.link_id); - if (index >= 0) { - this.links[index] = link; - this.mapChild.reload(); // temporary invocation - } - } - if (o.action === 'link.deleted') { - const link: Link = o.event; - const index = this.links.findIndex((l: Link) => l.link_id === link.link_id); - if (index >= 0) { - this.links.splice(index, 1); - this.mapChild.reload(); // temporary invocation - } - } - }); + this.projectWebServiceHandler.connect(this.ws); } - setUpMapCallbacks(project: Project) { + const selectionManager = new SelectionManager(this.nodesDataSource, this.linksDataSource, new InRectangleHelper()); + this.mapChild.graphLayout.getNodesWidget().setOnContextMenuCallback((event: any, node: Node) => { this.nodeContextMenu.open(node, event.clientY, event.clientX); }); this.mapChild.graphLayout.getNodesWidget().setOnNodeClickedCallback((event: any, node: Node) => { + selectionManager.setSelectedNodes([node]); if (this.drawLineMode) { this.nodeSelectInterfaceMenu.open(node, event.clientY, event.clientX); } }); this.mapChild.graphLayout.getNodesWidget().setOnNodeDraggedCallback((event: any, node: Node) => { - const index = this.nodes.findIndex((n: Node) => n.node_id === node.node_id); - if (index >= 0) { - this.nodes[index] = node; - this.mapChild.reload(); // temporary invocation - - this.nodeService - .updatePosition(this.server, node, node.x, node.y) - .subscribe((n: Node) => { - this.nodes[index] = node; - this.mapChild.reload(); // temporary invocation - }); - } + this.nodesDataSource.update(node); + this.nodeService + .updatePosition(this.server, node, node.x, node.y) + .subscribe((n: Node) => { + this.nodesDataSource.update(n); + }); }); + + selectionManager.subscribe(this.mapChild.graphLayout.getSelectionTool().rectangleSelected); } onNodeCreation(appliance: Appliance) { @@ -220,8 +193,7 @@ export class ProjectMapComponent implements OnInit { this.projectService .nodes(this.server, this.project.project_id) .subscribe((nodes: Node[]) => { - this.nodes = nodes; - this.mapChild.reload(); + this.nodesDataSource.set(nodes); }); }); } @@ -255,13 +227,22 @@ export class ProjectMapComponent implements OnInit { }); } - public turnOnDrawLineMode() { - this.drawLineMode = true; + public toggleDrawLineMode() { + this.drawLineMode = !this.drawLineMode; + if (!this.drawLineMode) { + this.mapChild.graphLayout.getDrawingLineTool().stop(); + } } - public turnOffDrawLineMode() { - this.drawLineMode = false; - this.mapChild.graphLayout.getDrawingLineTool().stop(); + public toggleMovingMode() { + this.movingMode = !this.movingMode; + if (this.movingMode) { + this.mapChild.graphLayout.getSelectionTool().deactivate(); + this.mapChild.graphLayout.getMovingTool().activate(); + } else { + this.mapChild.graphLayout.getMovingTool().deactivate(); + this.mapChild.graphLayout.getSelectionTool().activate(); + } } public onChooseInterface(event) { @@ -284,8 +265,7 @@ export class ProjectMapComponent implements OnInit { .createLink(this.server, source_node, source_port, target_node, target_port) .subscribe(() => { this.projectService.links(this.server, this.project.project_id).subscribe((links: Link[]) => { - this.links = links; - this.mapChild.reload(); + this.linksDataSource.set(links); }); }); } diff --git a/src/app/shared/databases/node-database.ts b/src/app/shared/databases/node-database.ts deleted file mode 100644 index e659bb12..00000000 --- a/src/app/shared/databases/node-database.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {Node} from "../../cartography/shared/models/node.model"; - -export class Database { - -} - -export class NodeDatabase extends Database { - -} diff --git a/src/app/shared/handlers/project-web-service-handler.spec.ts b/src/app/shared/handlers/project-web-service-handler.spec.ts new file mode 100644 index 00000000..64adda2f --- /dev/null +++ b/src/app/shared/handlers/project-web-service-handler.spec.ts @@ -0,0 +1,123 @@ +import {ProjectWebServiceHandler, WebServiceMessage} from "./project-web-service-handler"; +import {Subject} from "rxjs/Subject"; +import {inject, TestBed} from "@angular/core/testing"; +import {NodesDataSource} from "../../cartography/shared/datasources/nodes-datasource"; +import {LinksDataSource} from "../../cartography/shared/datasources/links-datasource"; +import {Node} from "../../cartography/shared/models/node"; +import {Link} from "../../cartography/shared/models/link"; + + + +describe('ProjectWebServiceHandler', () => { + let ws: Subject; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ProjectWebServiceHandler, NodesDataSource, LinksDataSource] + }); + + ws = new Subject(); + }); + + it('should be created', inject([ProjectWebServiceHandler], (service: ProjectWebServiceHandler) => { + expect(service).toBeTruthy(); + })); + + it('node should be added', inject([ProjectWebServiceHandler, NodesDataSource], + (service: ProjectWebServiceHandler, nodesDataSource: NodesDataSource) => { + spyOn(nodesDataSource, 'add'); + + service.connect(ws); + + const message = new WebServiceMessage(); + message.action = "node.created"; + message.event = new Node(); + + ws.next(message); + + expect(service).toBeTruthy(); + expect(nodesDataSource.add).toHaveBeenCalledWith(message.event); + })); + + it('node should be updated', inject([ProjectWebServiceHandler, NodesDataSource], + (service: ProjectWebServiceHandler, nodesDataSource: NodesDataSource) => { + spyOn(nodesDataSource, 'update'); + + service.connect(ws); + + const message = new WebServiceMessage(); + message.action = "node.updated"; + message.event = new Node(); + + ws.next(message); + + expect(service).toBeTruthy(); + expect(nodesDataSource.update).toHaveBeenCalledWith(message.event); + })); + + + it('node should be removed', inject([ProjectWebServiceHandler, NodesDataSource], + (service: ProjectWebServiceHandler, nodesDataSource: NodesDataSource) => { + spyOn(nodesDataSource, 'remove'); + + service.connect(ws); + + const message = new WebServiceMessage(); + message.action = "node.deleted"; + message.event = new Node(); + + ws.next(message); + + expect(service).toBeTruthy(); + expect(nodesDataSource.remove).toHaveBeenCalledWith(message.event); + })); + + it('link should be added', inject([ProjectWebServiceHandler, LinksDataSource], + (service: ProjectWebServiceHandler, linksDataSource: LinksDataSource) => { + spyOn(linksDataSource, 'add'); + + service.connect(ws); + + const message = new WebServiceMessage(); + message.action = "link.created"; + message.event = new Link(); + + ws.next(message); + + expect(service).toBeTruthy(); + expect(linksDataSource.add).toHaveBeenCalledWith(message.event); + })); + + it('link should be updated', inject([ProjectWebServiceHandler, LinksDataSource], + (service: ProjectWebServiceHandler, linksDataSource: LinksDataSource) => { + spyOn(linksDataSource, 'update'); + + service.connect(ws); + + const message = new WebServiceMessage(); + message.action = "link.updated"; + message.event = new Link(); + + ws.next(message); + + expect(service).toBeTruthy(); + expect(linksDataSource.update).toHaveBeenCalledWith(message.event); + })); + + + it('link should be removed', inject([ProjectWebServiceHandler, LinksDataSource], + (service: ProjectWebServiceHandler, linksDataSource: LinksDataSource) => { + spyOn(linksDataSource, 'remove'); + + service.connect(ws); + + const message = new WebServiceMessage(); + message.action = "link.deleted"; + message.event = new Link(); + + ws.next(message); + + expect(service).toBeTruthy(); + expect(linksDataSource.remove).toHaveBeenCalledWith(message.event); + })); +}); diff --git a/src/app/shared/handlers/project-web-service-handler.ts b/src/app/shared/handlers/project-web-service-handler.ts new file mode 100644 index 00000000..ad65b360 --- /dev/null +++ b/src/app/shared/handlers/project-web-service-handler.ts @@ -0,0 +1,41 @@ +import {Injectable} from "@angular/core"; +import {NodesDataSource} from "../../cartography/shared/datasources/nodes-datasource"; +import {LinksDataSource} from "../../cartography/shared/datasources/links-datasource"; +import {Subject} from "rxjs/Subject"; +import {Link} from "../../cartography/shared/models/link"; +import {Node} from "../../cartography/shared/models/node"; + + +export class WebServiceMessage { + action: string; + event: Node | Link; +} + +@Injectable() +export class ProjectWebServiceHandler { + constructor(private nodesDataSource: NodesDataSource, + private linksDataSource: LinksDataSource) {} + + public connect(ws: Subject) { + ws.subscribe((message: WebServiceMessage) => { + if (message.action === 'node.updated') { + this.nodesDataSource.update(message.event as Node); + } + if (message.action === 'node.created') { + this.nodesDataSource.add(message.event as Node); + } + if (message.action === 'node.deleted') { + this.nodesDataSource.remove(message.event as Node); + } + if (message.action === 'link.created') { + this.linksDataSource.add(message.event as Link); + } + if (message.action === 'link.updated') { + this.linksDataSource.update(message.event as Link); + } + if (message.action === 'link.deleted') { + this.linksDataSource.remove(message.event as Link); + } + }); + } +} diff --git a/src/app/shared/node-context-menu/actions/start-node-action/start-node-action.component.ts b/src/app/shared/node-context-menu/actions/start-node-action/start-node-action.component.ts index a2c6f65f..f13e7b73 100644 --- a/src/app/shared/node-context-menu/actions/start-node-action/start-node-action.component.ts +++ b/src/app/shared/node-context-menu/actions/start-node-action/start-node-action.component.ts @@ -1,7 +1,7 @@ import {Component, Input, OnInit} from '@angular/core'; import {Server} from "../../../models/server"; import {NodeService} from "../../../services/node.service"; -import {Node} from "../../../../cartography/shared/models/node.model"; +import {Node} from "../../../../cartography/shared/models/node"; @Component({ diff --git a/src/app/shared/node-context-menu/actions/stop-node-action/stop-node-action.component.ts b/src/app/shared/node-context-menu/actions/stop-node-action/stop-node-action.component.ts index 5cb8fc47..3885f45d 100644 --- a/src/app/shared/node-context-menu/actions/stop-node-action/stop-node-action.component.ts +++ b/src/app/shared/node-context-menu/actions/stop-node-action/stop-node-action.component.ts @@ -1,7 +1,7 @@ import {Component, Input, OnInit} from '@angular/core'; import {Server} from "../../../models/server"; import {NodeService} from "../../../services/node.service"; -import {Node} from "../../../../cartography/shared/models/node.model"; +import {Node} from "../../../../cartography/shared/models/node"; @Component({ diff --git a/src/app/shared/node-context-menu/node-context-menu.component.ts b/src/app/shared/node-context-menu/node-context-menu.component.ts index cc397011..f37b1a8f 100644 --- a/src/app/shared/node-context-menu/node-context-menu.component.ts +++ b/src/app/shared/node-context-menu/node-context-menu.component.ts @@ -1,7 +1,7 @@ import {ChangeDetectorRef, Component, Input, OnInit, ViewChild} from '@angular/core'; import {MatMenuTrigger} from "@angular/material"; import {DomSanitizer} from "@angular/platform-browser"; -import {Node} from "../../cartography/shared/models/node.model"; +import {Node} from "../../cartography/shared/models/node"; import {Server} from "../models/server"; diff --git a/src/app/shared/node-select-interface/node-select-interface.component.ts b/src/app/shared/node-select-interface/node-select-interface.component.ts index 74558b00..ecc3818f 100644 --- a/src/app/shared/node-select-interface/node-select-interface.component.ts +++ b/src/app/shared/node-select-interface/node-select-interface.component.ts @@ -1,7 +1,7 @@ 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/shared/models/node.model"; +import {Node} from "../../cartography/shared/models/node"; import {Port} from "../models/port"; diff --git a/src/app/shared/services/appliance.service.spec.ts b/src/app/shared/services/appliance.service.spec.ts index 7654781f..33678df6 100644 --- a/src/app/shared/services/appliance.service.spec.ts +++ b/src/app/shared/services/appliance.service.spec.ts @@ -1,15 +1,47 @@ -import { TestBed, inject } from '@angular/core/testing'; +import { TestBed, } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpClient } from "@angular/common/http"; import { ApplianceService } from './appliance.service'; +import { Server } from '../models/server'; +import { HttpServer } from './http-server.service'; + + describe('ApplianceService', () => { + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; + let service: ApplianceService; + beforeEach(() => { TestBed.configureTestingModule({ - providers: [ApplianceService] + imports: [ + HttpClientTestingModule + ], + providers: [ + ApplianceService, + HttpServer + ] }); + + httpClient = TestBed.get(HttpClient); + httpTestingController = TestBed.get(HttpTestingController); + service = TestBed.get(ApplianceService); }); - // it('should be created', inject([ApplianceService], (service: ApplianceService) => { - // expect(service).toBeTruthy(); - // })); + afterEach(() => { + httpTestingController.verify(); + }); + + it('should ask for the list from server', () => { + const server = new Server(); + server.ip = "127.0.0.1"; + server.port = 3080; + server.authorization = "none"; + + service.list(server).subscribe(); + + httpTestingController.expectOne('http://127.0.0.1:3080/v2/appliances'); + + }); }); diff --git a/src/app/shared/services/appliance.service.ts b/src/app/shared/services/appliance.service.ts index b25812d9..7a00edff 100644 --- a/src/app/shared/services/appliance.service.ts +++ b/src/app/shared/services/appliance.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import { Server } from "../models/server"; import { HttpServer } from "./http-server.service"; import {Appliance} from "../models/appliance"; +import {Observable} from "rxjs/Observable"; @Injectable() export class ApplianceService { @@ -13,8 +13,7 @@ export class ApplianceService { list(server: Server): Observable { return this.httpServer - .get(server, '/appliances') - .map(response => response.json() as Appliance[]); + .get(server, '/appliances') as Observable; } } diff --git a/src/app/shared/services/http-server.service.spec.ts b/src/app/shared/services/http-server.service.spec.ts index 75923814..1950af56 100644 --- a/src/app/shared/services/http-server.service.spec.ts +++ b/src/app/shared/services/http-server.service.spec.ts @@ -1,15 +1,146 @@ -import { TestBed, inject } from '@angular/core/testing'; +import { TestBed, } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpClient } from "@angular/common/http"; +import { Server } from '../models/server'; import { HttpServer } from './http-server.service'; + describe('HttpServer', () => { + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; + let service: HttpServer; + let server: Server; + beforeEach(() => { TestBed.configureTestingModule({ - providers: [HttpServer] + imports: [ + HttpClientTestingModule + ], + providers: [ + HttpServer + ] }); + + httpClient = TestBed.get(HttpClient); + httpTestingController = TestBed.get(HttpTestingController); + service = TestBed.get(HttpServer); + + server = new Server(); + server.ip = "127.0.0.1"; + server.port = 3080; + server.authorization = "none"; }); - // it('should be created', inject([HttpServer], (service: HttpServer) => { - // expect(service).toBeTruthy(); - // })); + afterEach(() => { + httpTestingController.verify(); + }); + + it('should make GET query for get method', () => { + service.get(server, '/test').subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("GET"); + expect(req.request.responseType).toEqual("json"); + }); + + it('should make GET query for getText method', () => { + service.getText(server, '/test').subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("GET"); + expect(req.request.responseType).toEqual("text"); + }); + + it('should make GET query for getText method and preserve options', () => { + service.getText(server, '/test', { + headers: { + 'CustomHeader': 'value' + }, + responseType: 'text' + }).subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("GET"); + expect(req.request.responseType).toEqual("text"); + }); + + it('should make POST query for post method', () => { + service.post(server, '/test', {test: "1"}).subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("POST"); + expect(req.request.responseType).toEqual("json"); + }); + + it('should make PUT query for put method', () => { + service.put(server, '/test', {test: "1"}).subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("PUT"); + expect(req.request.responseType).toEqual("json"); + }); + + it('should make DELETE query for delete method', () => { + service.delete(server, '/test').subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("DELETE"); + expect(req.request.responseType).toEqual("json"); + }); + + it('should make PATCH query for patch method', () => { + service.patch(server, '/test', {test: "1"}).subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("PATCH"); + expect(req.request.responseType).toEqual("json"); + }); + + it('should make HEAD query for head method', () => { + service.head(server, '/test').subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("HEAD"); + expect(req.request.responseType).toEqual("json"); + }); + + it('should make OPTIONS query for options method', () => { + service.options(server, '/test').subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("OPTIONS"); + expect(req.request.responseType).toEqual("json"); + }); + + it('should add headers for `basic` authorization', () => { + server.authorization = "basic"; + server.login = "login"; + server.password = "password"; + + service.get(server, '/test').subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("GET"); + expect(req.request.responseType).toEqual("json"); + expect(req.request.headers.get('Authorization')).toEqual('Basic bG9naW46cGFzc3dvcmQ='); + }); + + it('should add headers for `basic` authorization and preserve headers', () => { + server.authorization = "basic"; + server.login = "login"; + server.password = "password"; + + service.get(server, '/test', { + headers: { + 'CustomHeader': 'value' + } + }).subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/test'); + expect(req.request.method).toEqual("GET"); + expect(req.request.responseType).toEqual("json"); + expect(req.request.headers.get('Authorization')).toEqual('Basic bG9naW46cGFzc3dvcmQ='); + expect(req.request.headers.get('CustomHeader')).toEqual('value'); + }); }); diff --git a/src/app/shared/services/http-server.service.ts b/src/app/shared/services/http-server.service.ts index 3276c68f..d9aee35d 100644 --- a/src/app/shared/services/http-server.service.ts +++ b/src/app/shared/services/http-server.service.ts @@ -1,64 +1,132 @@ import { Injectable } from '@angular/core'; +import {HttpHeaders, HttpClient, HttpParams} from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; -import {Headers, Http, RequestOptions, RequestOptionsArgs, Response} from "@angular/http"; + import {Server} from "../models/server"; + +/* tslint:disable:interface-over-type-literal */ +export type JsonOptions = { + headers?: HttpHeaders | { + [header: string]: string | string[]; + }; + observe?: 'body'; + params?: HttpParams | { + [param: string]: string | string[]; + }; + reportProgress?: boolean; + responseType?: 'json'; + withCredentials?: boolean; +}; + +export type TextOptions = { + headers?: HttpHeaders | { + [header: string]: string | string[]; + }; + observe?: 'body'; + params?: HttpParams | { + [param: string]: string | string[]; + }; + reportProgress?: boolean; + responseType: 'text'; + withCredentials?: boolean; +}; + +export type HeadersOptions = { + headers?: HttpHeaders | { + [header: string]: string | string[]; + }; +}; +/* tslint:enable:interface-over-type-literal */ + + @Injectable() export class HttpServer { - constructor(private http: Http) { } + constructor(private http: HttpClient) { } - get(server: Server, url: string, options?: RequestOptionsArgs): Observable { - options = this.getOptionsForServer(server, url, options); - return this.http.get(url, options); + get(server: Server, url: string, options?: JsonOptions): Observable { + options = this.getJsonOptions(options); + const intercepted = this.getOptionsForServer(server, url, options); + return this.http.get(intercepted.url, intercepted.options as JsonOptions); } - post(server: Server, url: string, body: any, options?: RequestOptionsArgs): Observable { - options = this.getOptionsForServer(server, url, options); - return this.http.post(url, body, options); + getText(server: Server, url: string, options?: TextOptions): Observable { + options = this.getTextOptions(options); + const intercepted = this.getOptionsForServer(server, url, options); + return this.http.get(intercepted.url, intercepted.options as TextOptions); } - put(server: Server, url: string, body: any, options?: RequestOptionsArgs): Observable { - options = this.getOptionsForServer(server, url, options); - return this.http.put(url, body, options); + post(server: Server, url: string, body: any | null, options?: JsonOptions): Observable { + options = this.getJsonOptions(options); + const intercepted = this.getOptionsForServer(server, url, options); + return this.http.post(intercepted.url, body, intercepted.options); } - delete(server: Server, url: string, options?: RequestOptionsArgs): Observable { - options = this.getOptionsForServer(server, url, options); - return this.http.delete(url, options); + put(server: Server, url: string, body: any, options?: JsonOptions): Observable { + options = this.getJsonOptions(options); + const intercepted = this.getOptionsForServer(server, url, options); + return this.http.put(intercepted.url, body, intercepted.options); } - patch(server: Server, url: string, body: any, options?: RequestOptionsArgs): Observable { - options = this.getOptionsForServer(server, url, options); - return this.http.patch(url, body, options); + delete(server: Server, url: string, options?: JsonOptions): Observable { + options = this.getJsonOptions(options); + const intercepted = this.getOptionsForServer(server, url, options); + return this.http.delete(intercepted.url, intercepted.options); } - head(server: Server, url: string, options?: RequestOptionsArgs): Observable { - options = this.getOptionsForServer(server, url, options); - return this.http.patch(url, options); + patch(server: Server, url: string, body: any, options?: JsonOptions): Observable { + options = this.getJsonOptions(options); + const intercepted = this.getOptionsForServer(server, url, options); + return this.http.patch(intercepted.url, body, intercepted.options); } - options(server: Server, url: string, options?: RequestOptionsArgs): Observable { - options = this.getOptionsForServer(server, url, options); - return this.http.options(url, options); + head(server: Server, url: string, options?: JsonOptions): Observable { + options = this.getJsonOptions(options); + const intercepted = this.getOptionsForServer(server, url, options); + return this.http.head(intercepted.url, intercepted.options); } - private getOptionsForServer(server: Server, url: string, options) { - if (options === undefined) { - options = new RequestOptions(); + options(server: Server, url: string, options?: JsonOptions): Observable { + options = this.getJsonOptions(options); + const intercepted = this.getOptionsForServer(server, url, options); + return this.http.options(intercepted.url, intercepted.options); + } + + private getJsonOptions(options: JsonOptions): JsonOptions { + if (!options) { + return { + responseType: "json" + }; } - options.url = `http://${server.ip}:${server.port}/v2${url}`; + return options; + } - if (options.headers === null) { - options.headers = new Headers(); + private getTextOptions(options: TextOptions): TextOptions { + if (!options) { + return { + responseType: "text" + }; + } + return options; + } + + private getOptionsForServer(server: Server, url: string, options: T) { + url = `http://${server.ip}:${server.port}/v2${url}`; + + if (!options.headers) { + options.headers = {}; } if (server.authorization === "basic") { const credentials = btoa(`${server.login}:${server.password}`); - options.headers.append('Authorization', `Basic ${credentials}`); + options.headers['Authorization'] = `Basic ${credentials}`; } - return options; + return { + url: url, + options: options + }; } } diff --git a/src/app/shared/services/link.service.ts b/src/app/shared/services/link.service.ts index 7cb5cd0e..a3bf09e4 100644 --- a/src/app/shared/services/link.service.ts +++ b/src/app/shared/services/link.service.ts @@ -1,11 +1,9 @@ import { Injectable } from '@angular/core'; -import { Node } from '../../cartography/shared/models/node.model'; -import { Observable } from 'rxjs/Observable'; +import { Node } from '../../cartography/shared/models/node'; import 'rxjs/add/operator/map'; import { Server } from "../models/server"; import { HttpServer } from "./http-server.service"; -import {Response} from "@angular/http"; import {Port} from "../models/port"; @Injectable() @@ -15,7 +13,7 @@ export class LinkService { constructor(private httpServer: HttpServer) { } createLink( - server: Server, source_node: Node, source_port: Port, target_node: Node, target_port: Port): Observable { + server: Server, source_node: Node, source_port: Port, target_node: Node, target_port: Port) { return this.httpServer .post( server, diff --git a/src/app/shared/services/node.service.ts b/src/app/shared/services/node.service.ts index 62ff87d1..0ecedcf6 100644 --- a/src/app/shared/services/node.service.ts +++ b/src/app/shared/services/node.service.ts @@ -1,13 +1,12 @@ import { Injectable } from '@angular/core'; import { Project } from '../models/project'; -import { Node } from '../../cartography/shared/models/node.model'; +import { Node } from '../../cartography/shared/models/node'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import { Server } from "../models/server"; import { HttpServer } from "./http-server.service"; import {Appliance} from "../models/appliance"; -import {Response} from "@angular/http"; @Injectable() @@ -15,21 +14,19 @@ export class NodeService { constructor(private httpServer: HttpServer) { } - start(server: Server, node: Node): Observable { + start(server: Server, node: Node) { return this.httpServer - .post(server, `/projects/${node.project_id}/nodes/${node.node_id}/start`, {}) - .map(response => response.json() as Node); + .post(server, `/projects/${node.project_id}/nodes/${node.node_id}/start`, {}); } - stop(server: Server, node: Node): Observable { + stop(server: Server, node: Node) { return this.httpServer - .post(server, `/projects/${node.project_id}/nodes/${node.node_id}/stop`, {}) - .map(response => response.json() as Node); + .post(server, `/projects/${node.project_id}/nodes/${node.node_id}/stop`, {}); } createFromAppliance( server: Server, project: Project, appliance: Appliance, - x: number, y: number, compute_id: string): Observable { + x: number, y: number, compute_id: string) { return this.httpServer .post( server, @@ -39,10 +36,9 @@ export class NodeService { updatePosition(server: Server, node: Node, x: number, y: number): Observable { return this.httpServer - .put(server, `/projects/${node.project_id}/nodes/${node.node_id}`, { + .put(server, `/projects/${node.project_id}/nodes/${node.node_id}`, { 'x': x, 'y': y - }) - .map(response => response.json() as Node); + }); } } diff --git a/src/app/shared/services/project.service.ts b/src/app/shared/services/project.service.ts index 985c3bfc..c8f83440 100644 --- a/src/app/shared/services/project.service.ts +++ b/src/app/shared/services/project.service.ts @@ -1,53 +1,47 @@ import { Injectable } from '@angular/core'; import { Project } from '../models/project'; -import { Node } from '../../cartography/shared/models/node.model'; +import { Node } from '../../cartography/shared/models/node'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; -import { Link } from "../../cartography/shared/models/link.model"; +import { Link } from "../../cartography/shared/models/link"; import { Server } from "../models/server"; import { HttpServer } from "./http-server.service"; -import {Drawing} from "../../cartography/shared/models/drawing.model"; +import {Drawing} from "../../cartography/shared/models/drawing"; @Injectable() export class ProjectService { constructor(private httpServer: HttpServer) { } - get(server: Server, project_id: string): Observable { + get(server: Server, project_id: string) { return this.httpServer - .get(server, `/projects/${project_id}`) - .map(response => response.json() as Project); + .get(server, `/projects/${project_id}`); } - open(server: Server, project_id: string): Observable { + open(server: Server, project_id: string) { return this.httpServer - .post(server, `/projects/${project_id}/open`, {}) - .map(response => response.json() as Project); + .post(server, `/projects/${project_id}/open`, {}); } - list(server: Server): Observable { + list(server: Server) { return this.httpServer - .get(server, '/projects') - .map(response => response.json() as Project[]); + .get(server, '/projects'); } - nodes(server: Server, project_id: string): Observable { + nodes(server: Server, project_id: string) { return this.httpServer - .get(server, `/projects/${project_id}/nodes`) - .map(response => response.json() as Node[]); + .get(server, `/projects/${project_id}/nodes`); } - links(server: Server, project_id: string): Observable { + links(server: Server, project_id: string) { return this.httpServer - .get(server, `/projects/${project_id}/links`) - .map(response => response.json() as Link[]); + .get(server, `/projects/${project_id}/links`); } - drawings(server: Server, project_id: string): Observable { + drawings(server: Server, project_id: string) { return this.httpServer - .get(server, `/projects/${project_id}/drawings`) - .map(response => response.json() as Drawing[]); + .get(server, `/projects/${project_id}/drawings`); } delete(server: Server, project_id: string): Observable { diff --git a/src/app/shared/services/snapshot.service.ts b/src/app/shared/services/snapshot.service.ts index c08a1d17..047950fa 100644 --- a/src/app/shared/services/snapshot.service.ts +++ b/src/app/shared/services/snapshot.service.ts @@ -10,16 +10,14 @@ export class SnapshotService { constructor(private httpServer: HttpServer) { } - create(server: Server, project_id: string, snapshot: Snapshot): Observable { + create(server: Server, project_id: string, snapshot: Snapshot) { return this.httpServer - .post(server, `/projects/${project_id}/snapshots`, snapshot) - .map(response => response.json() as Snapshot); + .post(server, `/projects/${project_id}/snapshots`, snapshot); } - list(server: Server, project_id: string): Observable { + list(server: Server, project_id: string) { return this.httpServer - .get(server, `/projects/${project_id}/snapshots`) - .map(response => response.json() as Snapshot[]); + .get(server, `/projects/${project_id}/snapshots`); } } diff --git a/src/app/shared/services/symbol.service.ts b/src/app/shared/services/symbol.service.ts index 712d72a7..c60dd618 100644 --- a/src/app/shared/services/symbol.service.ts +++ b/src/app/shared/services/symbol.service.ts @@ -6,7 +6,7 @@ import 'rxjs/add/operator/map'; import 'rxjs/add/observable/forkJoin'; import 'rxjs/add/observable/of'; -import { Symbol } from '../models/symbol'; +import { Symbol } from '../../cartography/shared/models/symbol'; import { Server } from "../models/server"; import { HttpServer } from "./http-server.service"; @@ -26,7 +26,7 @@ export class SymbolService { load(server: Server): Observable { this.list(server).subscribe((symbols: Symbol[]) => { const streams = symbols.map(symbol => this.raw(server, symbol.symbol_id)); - Observable.forkJoin(streams).subscribe((results: string[]) => { + Observable.forkJoin(streams).subscribe((results) => { symbols.forEach((symbol: Symbol, i: number) => { symbol.raw = results[i]; }); @@ -36,16 +36,14 @@ export class SymbolService { return this.symbols.asObservable(); } - list(server: Server): Observable { + list(server: Server) { return this.httpServer - .get(server, '/symbols') - .map(response => response.json() as Symbol[]); + .get(server, '/symbols'); } - raw(server: Server, symbol_id: string): Observable { + raw(server: Server, symbol_id: string) { const encoded_uri = encodeURI(symbol_id); return this.httpServer - .get(server, `/symbols/${encoded_uri}/raw`) - .map(response => response.text() as string); + .getText(server, `/symbols/${encoded_uri}/raw`); } } diff --git a/src/app/shared/services/version.service.ts b/src/app/shared/services/version.service.ts index 35f51a15..adf4e5ab 100644 --- a/src/app/shared/services/version.service.ts +++ b/src/app/shared/services/version.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; @@ -13,9 +12,8 @@ export class VersionService { constructor(private httpServer: HttpServer) { } - get(server: Server): Observable { + get(server: Server) { return this.httpServer - .get(server, '/version') - .map(response => response.json() as Version); + .get(server, '/version'); } }