GUI2 Extract Topology view in to its own library
Change-Id: I45597d0902c99b5b3d606966866cc518011c54a0
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/karma.conf.js b/web/gui2-topo-lib/projects/gui2-topo-lib/karma.conf.js
new file mode 100644
index 0000000..4c5f8d0
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/karma.conf.js
@@ -0,0 +1,31 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma')
+ ],
+ client: {
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ dir: require('path').join(__dirname, '../../coverage'),
+ reports: ['html', 'lcovonly'],
+ fixWebpackSourcePaths: true
+ },
+ reporters: ['progress', 'kjhtml'],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['Chrome'],
+ singleRun: false
+ });
+};
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/ng-package.json b/web/gui2-topo-lib/projects/gui2-topo-lib/ng-package.json
new file mode 100644
index 0000000..a2e3192
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../dist/gui2-topo-lib",
+ "lib": {
+ "entryFile": "src/public_api.ts"
+ }
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/package.json b/web/gui2-topo-lib/projects/gui2-topo-lib/package.json
new file mode 100644
index 0000000..0584382
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "gui2-topo-lib",
+ "version": "2.0.0",
+ "peerDependencies": {
+ "@angular/common": "^7.0.0",
+ "@angular/core": "^7.0.0"
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/gui2-topo-lib.module.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/gui2-topo-lib.module.ts
new file mode 100644
index 0000000..643c3d7
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/gui2-topo-lib.module.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { TopologyRoutingModule } from './topology-routing.module';
+import { TopologyComponent } from './topology/topology.component';
+import { NoDeviceConnectedSvgComponent } from './layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component';
+import { InstanceComponent } from './panel/instance/instance.component';
+import { SummaryComponent } from './panel/summary/summary.component';
+import { ToolbarComponent } from './panel/toolbar/toolbar.component';
+import { DetailsComponent } from './panel/details/details.component';
+import { Gui2FwLibModule } from 'gui2-fw-lib';
+import { BackgroundSvgComponent } from './layer/backgroundsvg/backgroundsvg.component';
+import { ForceSvgComponent } from './layer/forcesvg/forcesvg.component';
+import { MapSvgComponent } from './layer/mapsvg/mapsvg.component';
+import { TopologyService } from './topology.service';
+import { DraggableDirective } from './layer/forcesvg/draggable/draggable.directive';
+import { ZoomableDirective } from './layer/zoomable.directive';
+import { MapSelectorComponent } from './panel/mapselector/mapselector.component';
+import { DeviceNodeSvgComponent} from './layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component';
+import { HostNodeSvgComponent } from './layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component';
+import { SubRegionNodeSvgComponent } from './layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component';
+import { LinkSvgComponent} from './layer/forcesvg/visuals/linksvg/linksvg.component';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import { GridsvgComponent } from './layer/gridsvg/gridsvg.component';
+import {TrafficService} from './traffic.service';
+
+/**
+ * ONOS GUI -- Topology View Module
+ *
+ * The main entry point is the TopologyComponent
+ *
+ * Note: This has been updated from onos-gui-1.0.0 where it was called 'topo2'
+ * whereas here it is now called 'topology'. This also merges in the old 'topo'
+ */
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ TopologyRoutingModule,
+ Gui2FwLibModule
+ ],
+ declarations: [
+ BackgroundSvgComponent,
+ DetailsComponent,
+ DeviceNodeSvgComponent,
+ ForceSvgComponent,
+ GridsvgComponent,
+ HostNodeSvgComponent,
+ InstanceComponent,
+ LinkSvgComponent,
+ MapSelectorComponent,
+ MapSvgComponent,
+ NoDeviceConnectedSvgComponent,
+ SubRegionNodeSvgComponent,
+ SummaryComponent,
+ ToolbarComponent,
+ TopologyComponent,
+ ZoomableDirective,
+ DraggableDirective,
+ ],
+ providers: [
+ TopologyService,
+ TrafficService
+ ],
+ exports: [
+ BackgroundSvgComponent,
+ DetailsComponent,
+ DeviceNodeSvgComponent,
+ ForceSvgComponent,
+ GridsvgComponent,
+ HostNodeSvgComponent,
+ InstanceComponent,
+ LinkSvgComponent,
+ MapSelectorComponent,
+ MapSvgComponent,
+ NoDeviceConnectedSvgComponent,
+ SubRegionNodeSvgComponent,
+ SummaryComponent,
+ ToolbarComponent,
+ TopologyComponent,
+ ZoomableDirective,
+ DraggableDirective,
+ ]
+})
+export class Gui2TopoLibModule { }
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.css
new file mode 100644
index 0000000..642c1c4
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.css
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ ONOS GUI -- Topology View (background) -- CSS file
+ */
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.html
new file mode 100644
index 0000000..9f8faf8
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.html
@@ -0,0 +1,29 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<!-- The transform here goes from a 0,0 centred grid of -180 to 180 of
+ longitude to -75 to 75 of latitude
+ It is mapped to a 2000x1000 SVG grid with -500,0 at the top left
+ (The SVG viewbox of ONOS is 1000x1000 - for the geo grid we wanted
+ to keep it the same height 1000 representing +75 latitude down to
+ -75 latitude, but double the width. Why 75? There's no city in the
+ world above 70 - Murmansk)
+ The 6.66 represents 1000/150 and the 5.55 represents 2000/360
+ The reason for the difference is that mercator projection widens
+ countries in the northern and southern extremities, and so
+ the map is squashed horizontally slightly here to compensate
+ (with no squashing the width would be 2400)-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" onos-mapsvg [map]="map" (mapBounds)="updatedBounds($event)"
+ transform="translate(500,500), scale(5.5555,6.666666)"/>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.spec.ts
new file mode 100644
index 0000000..77e5d55
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.spec.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BackgroundSvgComponent } from './backgroundsvg.component';
+import {MapSvgComponent} from '../mapsvg/mapsvg.component';
+import {from} from 'rxjs';
+import {HttpClient} from '@angular/common/http';
+import {LocMeta, LogService, ZoomUtils} from 'gui2-fw-lib';
+import {MapObject} from '../maputils';
+import {ForceSvgComponent} from '../forcesvg/forcesvg.component';
+
+import {DraggableDirective} from '../forcesvg/draggable/draggable.directive';
+import {DeviceNodeSvgComponent} from '../forcesvg/visuals/devicenodesvg/devicenodesvg.component';
+import {SubRegionNodeSvgComponent} from '../forcesvg/visuals/subregionnodesvg/subregionnodesvg.component';
+import {LinkSvgComponent} from '../forcesvg/visuals/linksvg/linksvg.component';
+import {HostNodeSvgComponent} from '../forcesvg/visuals/hostnodesvg/hostnodesvg.component';
+
+class MockHttpClient {
+ get() {
+ return from(['{"id":"app","icon":"nav_apps","cat":"PLATFORM","label":"Applications"}']);
+ }
+
+ subscribe() {}
+}
+
+describe('BackgroundSvgComponent', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: BackgroundSvgComponent;
+ let fixture: ComponentFixture<BackgroundSvgComponent>;
+ const testmap: MapObject = <MapObject>{
+ scale: 1.0,
+ id: 'test',
+ description: 'test map'
+ };
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+
+ TestBed.configureTestingModule({
+ declarations: [
+ BackgroundSvgComponent,
+ MapSvgComponent,
+ ForceSvgComponent,
+ DeviceNodeSvgComponent,
+ HostNodeSvgComponent,
+ SubRegionNodeSvgComponent,
+ LinkSvgComponent,
+ DraggableDirective
+ ],
+ providers: [
+ { provide: LogService, useValue: logSpy },
+ { provide: HttpClient, useClass: MockHttpClient },
+ ]
+ })
+ .compileComponents();
+
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BackgroundSvgComponent);
+ component = fixture.componentInstance;
+ component.map = testmap;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should convert latlong to xy', () => {
+ const result = ZoomUtils.convertGeoToCanvas(<LocMeta>{lat: 52, lng: -8});
+ expect(Math.round(result.x * 100)).toEqual(45556);
+ expect(Math.round(result.y * 100)).toEqual(15333);
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.ts
new file mode 100644
index 0000000..daa4fcb
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/backgroundsvg/backgroundsvg.component.ts
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {MapObject} from '../maputils';
+import {MapBounds, TopoZoomPrefs, LogService, ZoomUtils} from 'gui2-fw-lib';
+
+/**
+ * model of the topo2CurrentLayout attrs from BgZoom below
+ */
+export interface BgZoomAttrs {
+ offsetX: number;
+ offsetY: number;
+ scale: number;
+}
+
+/**
+ * model of the topo2CurrentLayout background zoom attrs from Layout below
+ */
+export interface BgZoom {
+ cfg: BgZoomAttrs;
+ usr?: BgZoomAttrs;
+}
+
+/**
+ * model of the topo2CurrentLayout breadcrumb from Layout below
+ */
+export interface LayoutCrumb {
+ id: string;
+ name: string;
+}
+
+/**
+ * Enum of the topo2CurrentRegion location type from Location below
+ */
+export enum LocationType {
+ NONE = 'none',
+ GEO = 'geo',
+ GRID = 'grid'
+}
+
+/**
+ * model of the topo2CurrentLayout WebSocket response
+ */
+export interface Layout {
+ id: string;
+ bgDefaultScale: number;
+ bgDesc: string;
+ bgFilePath: string;
+ bgId: string;
+ bgType: LocationType;
+ bgWarn: string;
+ bgZoom: BgZoom;
+ crumbs: LayoutCrumb[];
+ parent: string;
+ region: string;
+ regionName: string;
+}
+
+/**
+ * ONOS GUI -- Topology Background Layer View.
+ *
+ * TODO: consider that this layer has only one component the MapSvg and hence
+ * might be able to be eliminated
+ */
+@Component({
+ selector: '[onos-backgroundsvg]',
+ templateUrl: './backgroundsvg.component.html',
+ styleUrls: ['./backgroundsvg.component.css']
+})
+export class BackgroundSvgComponent {
+ @Input() map: MapObject;
+ @Output() zoomlevel = new EventEmitter<TopoZoomPrefs>();
+
+ layoutData: Layout = <Layout>{};
+
+ constructor(
+ private log: LogService
+ ) {
+ this.log.debug('BackgroundSvg constructed');
+ }
+
+ /**
+ * Called when ever the mapBounds event is raised by the MapSvgComponent
+ *
+ * @param bounds - the bounds of the newly loaded map in terms of Lat and Long
+ */
+ updatedBounds(bounds: MapBounds): void {
+ const zoomPrefs: TopoZoomPrefs =
+ ZoomUtils.convertBoundsToZoomLevel(bounds, this.log);
+
+ this.zoomlevel.emit(zoomPrefs);
+ }
+
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/draggable/draggable.directive.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/draggable/draggable.directive.spec.ts
new file mode 100644
index 0000000..94c61db
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/draggable/draggable.directive.spec.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { DraggableDirective } from './draggable.directive';
+import {inject, TestBed} from '@angular/core/testing';
+import {ElementRef} from '@angular/core';
+import {LogService} from 'gui2-fw-lib';
+
+export class MockElementRef extends ElementRef {
+ nativeElement = {};
+}
+
+describe('DraggableDirective', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let mockWindow: Window;
+
+ beforeEach(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ mockWindow = <any>{
+ navigator: {
+ userAgent: 'HeadlessChrome',
+ vendor: 'Google Inc.'
+ }
+ };
+
+ TestBed.configureTestingModule({
+ providers: [DraggableDirective,
+ { provide: LogService, useValue: logSpy },
+ { provide: 'Window', useFactory: (() => mockWindow ) },
+ { provide: ElementRef, useValue: mockWindow }
+ ]
+ });
+ logServiceSpy = TestBed.get(LogService);
+ });
+
+ it('should create an instance', inject([DraggableDirective], (directive: DraggableDirective) => {
+
+ expect(directive).toBeTruthy();
+ }));
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/draggable/draggable.directive.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/draggable/draggable.directive.ts
new file mode 100644
index 0000000..474d7a8
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/draggable/draggable.directive.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Directive,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnChanges, Output
+} from '@angular/core';
+import {ForceDirectedGraph, Node} from '../models';
+import * as d3 from 'd3';
+import {LogService, MetaUi, ZoomUtils} from 'gui2-fw-lib';
+import {BackgroundSvgComponent} from '../../backgroundsvg/backgroundsvg.component';
+
+@Directive({
+ selector: '[onosDraggableNode]'
+})
+export class DraggableDirective implements OnChanges {
+ @Input() draggableNode: Node;
+ @Input() draggableInGraph: ForceDirectedGraph;
+ @Output() newLocation = new EventEmitter<MetaUi>();
+
+ constructor(
+ private _element: ElementRef,
+ private log: LogService
+ ) {
+ this.log.debug('DraggableDirective constructed');
+ }
+
+ ngOnChanges() {
+ this.applyDraggableBehaviour(
+ this._element.nativeElement,
+ this.draggableNode,
+ this.draggableInGraph,
+ this.newLocation);
+ }
+
+ /**
+ * A method to bind a draggable behaviour to an svg element
+ */
+ applyDraggableBehaviour(element, node: Node, graph: ForceDirectedGraph, newLocation: EventEmitter<MetaUi>) {
+ const d3element = d3.select(element);
+
+ function started() {
+ /** Preventing propagation of dragstart to parent elements */
+ d3.event.sourceEvent.stopPropagation();
+
+ if (!d3.event.active) {
+ graph.simulation.alphaTarget(0.3).restart();
+ }
+
+ d3.event.on('drag', () => dragged()).on('end', () => ended());
+
+ function dragged() {
+ node.fx = d3.event.x;
+ node.fy = d3.event.y;
+ }
+
+ function ended() {
+ if (!d3.event.active) {
+ graph.simulation.alphaTarget(0);
+ }
+ newLocation.emit(ZoomUtils.convertXYtoGeo(node.fx, node.fy));
+
+ // node.fx = null;
+ // node.fy = null;
+ }
+ }
+
+ d3element.call(d3.drag()
+ .on('start', started));
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.css
new file mode 100644
index 0000000..addd41c
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.css
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ ONOS GUI -- Topology View (forces) -- CSS file
+ */
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.html
new file mode 100644
index 0000000..026ef87
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.html
@@ -0,0 +1,104 @@
+<!--
+~ Copyright 2019-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<svg:desc xmlns:svg="http://www.w3.org/2000/svg">The force layout layer. This is
+ an SVG component that displays Nodes (Devices, Hosts and SubRegions) and
+ Links. Positions of each are driven by a forces computation engine</svg:desc>
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" class="topo2-links">
+ <svg:desc>Topology links</svg:desc>
+ <!-- Template explanation: Creates an SVG Group and in
+ line 1) use the svg component onos-linksvg, setting it's link
+ Input parameter to the link item from the next line
+ line 2) Use the built in NgFor directive to iterate through the
+ set of links filtered by the filteredLinks() function.
+ line 3) feed the highlightPorts of this (forcesvg) component in to
+ the highlightsEnabled of the link component
+ line 5) when the onos-linksvg component emits the selectedEvent,
+ call the updateSelected() method of this (forcesvg) component
+ line 6) feed the scale of this (forcesvg) component in to the scale
+ of the link
+ -->
+ <svg:g onos-linksvg [link]="link"
+ *ngFor="let link of filteredLinks()"
+ [highlightsEnabled]="highlightPorts"
+ (selectedEvent)="updateSelected($event)"
+ [scale]="scale">
+ </svg:g>
+</svg:g>
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" class="topo2-nodes">
+ <svg:desc>Topology nodes</svg:desc>
+ <!-- Template explanation - create an SVG Group and
+ line 1) use the svg component onos-devicenodesvg, setting it's device
+ Input parameter to the device item from the next line
+ line 2) Use the built in NgFor directive to iterate through all
+ of the devices in the chosen layer index. The current iteration
+ is in the device variable
+ line 3) Use the onosDraggable directive and pass this device in to
+ its draggableNode Input parameter and setting the draggableInGraph
+ Input parameter to 'graph'
+ line 4) event handler of the draggable directive - causes the new location
+ to be written back to the server
+ line 5) when the onos-devicenodesvg component emits the selectedEvent,
+ call the updateSelected() method of this (forcesvg) component
+ line 6) feed the devicelabeltoggle of this (forcesvg) component in to
+ the labelToggle of the device
+ line 7) feed the scale of this (forcesvg) component in to the scale
+ of the device
+ -->
+ <svg:g onos-devicenodesvg [device]="device"
+ *ngFor="let device of regionData.devices[visibleLayerIdx()]"
+ onosDraggableNode [draggableNode]="device" [draggableInGraph]="graph"
+ (newLocation)="nodeMoved('device', device.id, $event)"
+ (selectedEvent)="updateSelected($event)"
+ [labelToggle]="deviceLabelToggle"
+ [scale]="scale">
+ <svg:desc>Device nodes</svg:desc>
+ </svg:g>
+ <!-- Template explanation - only display the hosts if 'showHosts' is set true -->
+ <svg:g *ngIf="showHosts">
+ <!-- Template explanation - create an SVG Group and
+ line 1) use the svg component onos-hostnodesvg, setting it's host
+ Input parameter to the host item from the next line
+ line 2) Use the built in NgFor directive to iterate through all
+ of the hosts in the chosen layer index. The current iteration
+ is in the 'host' variable
+ line 3) Use the onosDraggable directive and pass this host in to
+ its draggableNode Input parameter and setting the draggableInGraph
+ Input parameter to 'graph'
+ line 4) event handler of the draggable directive - causes the new location
+ to be written back to the server
+ line 5) when the onos-hostnodesvg component emits the selectedEvent
+ call the updateSelected() method of this (forcesvg) component
+ line 6) feed the hostLabelToggle of this (forcesvg) component in to
+ the labelToggle of the host
+ line 7) feed the scale of this (forcesvg) component in to the scale
+ of the host
+ -->
+ <svg:g onos-hostnodesvg [host]="host"
+ *ngFor="let host of regionData.hosts[visibleLayerIdx()]"
+ onosDraggableNode [draggableNode]="host" [draggableInGraph]="graph"
+ (newLocation)="nodeMoved('host', host.id, $event)"
+ (selectedEvent)="updateSelected($event)"
+ [labelToggle]="hostLabelToggle"
+ [scale]="scale">
+ <svg:desc>Host nodes</svg:desc>
+ </svg:g>
+ </svg:g>
+ <svg:g onos-subregionnodesvg [subRegion]="subRegion"
+ *ngFor="let subRegion of regionData.subregions"
+ onosDraggableNode [draggableNode]="subRegion" [draggableInGraph]="graph">
+ <svg:desc>Subregion nodes</svg:desc>
+ </svg:g>
+</svg:g>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.spec.ts
new file mode 100644
index 0000000..9a2ae0e
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.spec.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ForceSvgComponent } from './forcesvg.component';
+import {FnService, LogService} from 'gui2-fw-lib';
+import {DraggableDirective} from './draggable/draggable.directive';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {MapSvgComponent} from '../mapsvg/mapsvg.component';
+import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
+import {SubRegionNodeSvgComponent} from './visuals/subregionnodesvg/subregionnodesvg.component';
+import {HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
+import {LinkSvgComponent} from './visuals/linksvg/linksvg.component';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+describe('ForceSvgComponent', () => {
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: ForceSvgComponent;
+ let fixture: ComponentFixture<ForceSvgComponent>;
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+ windowMock = <any>{
+ location: <any>{
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true' },
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+
+ fs = new FnService(ar, logSpy, windowMock);
+
+ TestBed.configureTestingModule({
+ declarations: [
+ ForceSvgComponent,
+ DeviceNodeSvgComponent,
+ HostNodeSvgComponent,
+ SubRegionNodeSvgComponent,
+ LinkSvgComponent,
+ DraggableDirective,
+ MapSvgComponent
+ ],
+ providers: [
+ { provide: FnService, useValue: fs },
+ { provide: LogService, useValue: logSpy },
+ { provide: 'Window', useValue: windowMock },
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ForceSvgComponent);
+ component = fixture.debugElement.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
new file mode 100644
index 0000000..6910353
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
@@ -0,0 +1,517 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ HostListener,
+ Input,
+ OnChanges,
+ OnInit,
+ Output,
+ QueryList,
+ SimpleChange,
+ SimpleChanges,
+ ViewChildren
+} from '@angular/core';
+import {
+ LocMeta,
+ LogService,
+ MetaUi,
+ WebSocketService,
+ ZoomUtils
+} from 'gui2-fw-lib';
+import {
+ Device,
+ ForceDirectedGraph,
+ Host,
+ HostLabelToggle,
+ LabelToggle,
+ LayerType,
+ Link,
+ LinkHighlight,
+ Location,
+ ModelEventMemo,
+ ModelEventType,
+ Region,
+ RegionLink,
+ SubRegion,
+ UiElement
+} from './models';
+import {LocationType} from '../backgroundsvg/backgroundsvg.component';
+import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
+import { HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
+import { LinkSvgComponent} from './visuals/linksvg/linksvg.component';
+
+interface UpdateMeta {
+ id: string;
+ class: string;
+ memento: MetaUi;
+}
+
+/**
+ * ONOS GUI -- Topology Forces Graph Layer View.
+ *
+ * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
+ * This drives the whole Force graph
+ */
+@Component({
+ selector: '[onos-forcesvg]',
+ templateUrl: './forcesvg.component.html',
+ styleUrls: ['./forcesvg.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ForceSvgComponent implements OnInit, OnChanges {
+ @Input() deviceLabelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
+ @Input() hostLabelToggle: HostLabelToggle.Enum = HostLabelToggle.Enum.NONE;
+ @Input() showHosts: boolean = false;
+ @Input() highlightPorts: boolean = true;
+ @Input() onosInstMastership: string = '';
+ @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
+ @Input() selectedLink: RegionLink = null;
+ @Input() scale: number = 1;
+ @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
+ @Output() linkSelected = new EventEmitter<RegionLink>();
+ @Output() selectedNodeEvent = new EventEmitter<UiElement>();
+ public graph: ForceDirectedGraph;
+ private _options: { width, height } = { width: 800, height: 600 };
+
+ // References to the children of this component - these are created in the
+ // template view with the *ngFor and we get them by a query here
+ @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
+ @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
+ @ViewChildren(LinkSvgComponent) links: QueryList<LinkSvgComponent>;
+
+ constructor(
+ protected log: LogService,
+ private ref: ChangeDetectorRef,
+ protected wss: WebSocketService
+ ) {
+ this.selectedLink = null;
+ this.log.debug('ForceSvgComponent constructed');
+ }
+
+ /**
+ * Utility for extracting a node name from an endpoint string
+ * In some cases - have to remove the port number from the end of a device
+ * name
+ * @param endPtStr The end point name
+ */
+ private static extractNodeName(endPtStr: string): string {
+ const slash: number = endPtStr.indexOf('/');
+ if (slash === -1) {
+ return endPtStr;
+ } else {
+ const afterSlash = endPtStr.substr(slash + 1);
+ if (afterSlash === 'None') {
+ return endPtStr;
+ } else {
+ return endPtStr.substr(0, slash);
+ }
+ }
+ }
+
+ /**
+ * Recursive method to compare 2 objects attribute by attribute and update
+ * the first where a change is detected
+ * @param existingNode 1st object
+ * @param updatedNode 2nd object
+ */
+ private static updateObject(existingNode: Object, updatedNode: Object): number {
+ let changed: number = 0;
+ for (const key of Object.keys(updatedNode)) {
+ const o = updatedNode[key];
+ if (key === 'id') {
+ continue;
+ } else if (o && typeof o === 'object' && o.constructor === Object) {
+ changed += ForceSvgComponent.updateObject(existingNode[key], updatedNode[key]);
+ } else if (existingNode[key] !== updatedNode[key]) {
+ changed++;
+ existingNode[key] = updatedNode[key];
+ }
+ }
+ return changed;
+ }
+
+ @HostListener('window:resize', ['$event'])
+ onResize(event) {
+ this.graph.initSimulation(this.options);
+ this.log.debug('Simulation reinit after resize', event);
+ }
+
+ /**
+ * After the component is initialized create the Force simulation
+ * The list of devices, hosts and links will not have been receieved back
+ * from the WebSocket yet as this time - they will be updated later through
+ * ngOnChanges()
+ */
+ ngOnInit() {
+ // Receiving an initialized simulated graph from our custom d3 service
+ this.graph = new ForceDirectedGraph(this.options, this.log);
+
+ /** Binding change detection check on each tick
+ * This along with an onPush change detection strategy should enforce
+ * checking only when relevant! This improves scripting computation
+ * duration in a couple of tests I've made, consistently. Also, it makes
+ * sense to avoid unnecessary checks when we are dealing only with
+ * simulations data binding.
+ */
+ this.graph.ticker.subscribe((simulation) => {
+ // this.log.debug("Force simulation has ticked", simulation);
+ this.ref.markForCheck();
+ });
+ this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
+
+ }
+
+ /**
+ * When any one of the inputs get changed by a containing component, this
+ * gets called automatically. In addition this is called manually by
+ * topology.service when a response is received from the WebSocket from the
+ * server
+ *
+ * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
+ * The Links are added to the Link list of the simulation.
+ * Before they are added the Links are associated with Nodes based on their endPt
+ *
+ * @param changes - a list of changed @Input(s)
+ */
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['regionData']) {
+ const devices: Device[] =
+ changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
+ const hosts: Host[] =
+ changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
+ const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
+ this.graph.nodes = [];
+ if (devices) {
+ this.graph.nodes = devices;
+ }
+ if (hosts) {
+ this.graph.nodes = this.graph.nodes.concat(hosts);
+ }
+ if (subRegions) {
+ this.graph.nodes = this.graph.nodes.concat(subRegions);
+ }
+
+ // If a node has a fixed location then assign it to fx and fy so
+ // that it doesn't get affected by forces
+ this.graph.nodes
+ .forEach((n) => {
+ const loc: Location = <Location>n['location'];
+ if (loc && loc.locType === LocationType.GEO) {
+ const position: MetaUi =
+ ZoomUtils.convertGeoToCanvas(
+ <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
+ n.fx = position.x;
+ n.fy = position.y;
+ this.log.debug('Found node', n.id, 'with', loc.locType);
+ }
+ });
+
+ // Associate the endpoints of each link with a real node
+ this.graph.links = [];
+ for (const linkIdx of Object.keys(this.regionData.links)) {
+ const epA = ForceSvgComponent.extractNodeName(
+ this.regionData.links[linkIdx].epA);
+ this.regionData.links[linkIdx].source =
+ this.graph.nodes.find((node) =>
+ node.id === epA);
+ const epB = ForceSvgComponent.extractNodeName(
+ this.regionData.links[linkIdx].epB);
+ this.regionData.links[linkIdx].target =
+ this.graph.nodes.find((node) =>
+ node.id === epB);
+ this.regionData.links[linkIdx].index = Number(linkIdx);
+ }
+
+ this.graph.links = this.regionData.links;
+
+ this.graph.initSimulation(this.options);
+ this.graph.initNodes();
+ this.graph.initLinks();
+ this.log.debug('ForceSvgComponent input changed',
+ this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
+ }
+
+ this.ref.markForCheck();
+ }
+
+ /**
+ * Get the index of LayerType so it can drive the visibility of nodes and
+ * hosts on layers
+ */
+ visibleLayerIdx(): number {
+ const layerKeys: string[] = Object.keys(LayerType);
+ for (const idx in layerKeys) {
+ if (LayerType[layerKeys[idx]] === this.visibleLayer) {
+ return Number(idx);
+ }
+ }
+ return -1;
+ }
+
+ selectLink(link: RegionLink): void {
+ this.selectedLink = link;
+ this.linkSelected.emit(link);
+ }
+
+ get options() {
+ return this._options = {
+ width: window.innerWidth,
+ height: window.innerHeight
+ };
+ }
+
+ /**
+ * Iterate through all hosts and devices to deselect the previously selected
+ * node. The emit an event to the parent that lets it know the selection has
+ * changed.
+ * @param selectedNode the newly selected node
+ */
+ updateSelected(selectedNode: UiElement): void {
+ this.log.debug('Node or link selected', selectedNode ? selectedNode.id : 'none');
+ this.devices
+ .filter((d) =>
+ selectedNode === undefined || d.device.id !== selectedNode.id)
+ .forEach((d) => d.deselect());
+ this.hosts
+ .filter((h) =>
+ selectedNode === undefined || h.host.id !== selectedNode.id)
+ .forEach((h) => h.deselect());
+
+ this.links
+ .filter((l) =>
+ selectedNode === undefined || l.link.id !== selectedNode.id)
+ .forEach((l) => l.deselect());
+ // Push the changes back up to parent (Topology Component)
+ this.selectedNodeEvent.emit(selectedNode);
+ }
+
+ /**
+ * We want to filter links to show only those not related to hosts if the
+ * 'showHosts' flag has been switched off. If 'showHosts' is true, then
+ * display all links.
+ */
+ filteredLinks(): Link[] {
+ return this.regionData.links.filter((h) =>
+ this.showHosts ||
+ ((<Host>h.source).nodeType !== 'host' &&
+ (<Host>h.target).nodeType !== 'host'));
+ }
+
+ /**
+ * When changes happen in the model, then model events are sent up through the
+ * Web Socket
+ * @param type - the type of the change
+ * @param memo - a qualifier on the type
+ * @param subject - the item that the update is for
+ * @param data - the new definition of the item
+ */
+ handleModelEvent(type: ModelEventType, memo: ModelEventMemo, subject: string, data: UiElement): void {
+ switch (type) {
+ case ModelEventType.DEVICE_ADDED_OR_UPDATED:
+ if (memo === ModelEventMemo.ADDED) {
+ const loc = (<Device>data).location;
+ if (loc && loc.locType === LocationType.GEO) {
+ const position =
+ ZoomUtils.convertGeoToCanvas(<LocMeta>{ lng: loc.longOrX, lat: loc.latOrY});
+ (<Device>data).fx = position.x;
+ (<Device>data).fy = position.y;
+ this.log.debug('Using long', loc.longOrX, 'lat', loc.latOrY, '(', position.x, position.y, ')');
+ } else if (loc && loc.locType === LocationType.GRID) {
+ (<Device>data).fx = loc.longOrX;
+ (<Device>data).fy = loc.latOrY;
+ this.log.debug('Using grid', loc.longOrX, loc.latOrY);
+ } else {
+ (<Device>data).fx = null;
+ (<Device>data).fy = null;
+ // (<Device>data).x = 500;
+ // (<Device>data).y = 500;
+ }
+ this.graph.nodes.push(<Device>data);
+ this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
+ this.log.debug('Device added', (<Device>data).id);
+ } else if (memo === ModelEventMemo.UPDATED) {
+ const oldDevice: Device =
+ this.regionData.devices[this.visibleLayerIdx()]
+ .find((d) => d.id === subject);
+ const changes = ForceSvgComponent.updateObject(oldDevice, <Device>data);
+ if (changes > 0) {
+ this.log.debug('Device ', oldDevice.id, memo, ' - ', changes, 'changes');
+ }
+ } else {
+ this.log.warn('Device ', memo, ' - not yet implemented', data);
+ }
+ break;
+ case ModelEventType.HOST_ADDED_OR_UPDATED:
+ if (memo === ModelEventMemo.ADDED) {
+ this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
+ this.graph.nodes.push(<Host>data);
+ this.log.debug('Host added', (<Host>data).id);
+ } else if (memo === ModelEventMemo.UPDATED) {
+ const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
+ .find((h) => h.id === subject);
+ const changes = ForceSvgComponent.updateObject(oldHost, <Host>data);
+ if (changes > 0) {
+ this.log.debug('Host ', oldHost.id, memo, ' - ', changes, 'changes');
+ }
+ } else {
+ this.log.warn('Host change', memo, ' - unexpected');
+ }
+ break;
+ case ModelEventType.DEVICE_REMOVED:
+ if (memo === ModelEventMemo.REMOVED || memo === undefined) {
+ const removeIdx: number =
+ this.regionData.devices[this.visibleLayerIdx()]
+ .findIndex((d) => d.id === subject);
+ this.regionData.devices[this.visibleLayerIdx()].splice(removeIdx, 1);
+ this.removeRelatedLinks(subject);
+ this.log.debug('Device ', subject, 'removed. Links', this.regionData.links);
+ } else {
+ this.log.warn('Device removed - unexpected memo', memo);
+ }
+ break;
+ case ModelEventType.HOST_REMOVED:
+ if (memo === ModelEventMemo.REMOVED || memo === undefined) {
+ const removeIdx: number =
+ this.regionData.hosts[this.visibleLayerIdx()]
+ .findIndex((h) => h.id === subject);
+ this.regionData.hosts[this.visibleLayerIdx()].splice(removeIdx, 1);
+ this.removeRelatedLinks(subject);
+ this.log.warn('Host ', subject, 'removed');
+ } else {
+ this.log.warn('Host removed - unexpected memo', memo);
+ }
+ break;
+ case ModelEventType.LINK_ADDED_OR_UPDATED:
+ if (memo === ModelEventMemo.ADDED &&
+ this.regionData.links.findIndex((l) => l.id === subject) === -1) {
+ const listLen = this.regionData.links.push(<RegionLink>data);
+ const epA = ForceSvgComponent.extractNodeName(
+ this.regionData.links[listLen - 1].epA);
+ this.regionData.links[listLen - 1].source =
+ this.graph.nodes.find((node) =>
+ node.id === epA);
+ const epB = ForceSvgComponent.extractNodeName(
+ this.regionData.links[listLen - 1].epB);
+ this.regionData.links[listLen - 1].target =
+ this.graph.nodes.find((node) =>
+ node.id === epB);
+ this.log.debug('Link added', subject);
+ } else if (memo === ModelEventMemo.UPDATED) {
+ const oldLink = this.regionData.links.find((l) => l.id === subject);
+ const changes = ForceSvgComponent.updateObject(oldLink, <RegionLink>data);
+ this.log.debug('Link ', subject, '. Updated', changes, 'items');
+ } else {
+ this.log.warn('Link added or updated - unexpected memo', memo);
+ }
+ break;
+ default:
+ this.log.error('Unexpected model event', type, 'for', subject);
+ }
+ this.ref.markForCheck();
+ this.graph.initSimulation(this.options);
+ this.graph.initNodes();
+ this.graph.initLinks();
+ }
+
+ private removeRelatedLinks(subject: string) {
+ const len = this.regionData.links.length;
+ for (let i = 0; i < len; i++) {
+ const linkIdx = this.regionData.links.findIndex((l) =>
+ (ForceSvgComponent.extractNodeName(l.epA) === subject ||
+ ForceSvgComponent.extractNodeName(l.epB) === subject));
+ if (linkIdx >= 0) {
+ this.regionData.links.splice(linkIdx, 1);
+ this.log.debug('Link ', linkIdx, 'removed on attempt', i);
+ }
+ }
+ }
+
+ /**
+ * When traffic monitoring is turned on (A key) highlights will be sent back
+ * from the WebSocket through the Traffic Service
+ * @param devices - an array of device highlights
+ * @param hosts - an array of host highlights
+ * @param links - an array of link highlights
+ */
+ handleHighlights(devices: Device[], hosts: Host[], links: LinkHighlight[]): void {
+
+ if (devices.length > 0) {
+ this.log.debug(devices.length, 'Devices highlighted');
+ devices.forEach((dh) => {
+ const deviceComponent: DeviceNodeSvgComponent = this.devices.find((d) => d.device.id === dh.id );
+ if (deviceComponent) {
+ deviceComponent.ngOnChanges(
+ {'deviceHighlight': new SimpleChange(<Device>{}, dh, true)}
+ );
+ this.log.debug('Highlighting device', deviceComponent.device.id);
+ } else {
+ this.log.warn('Device component not found', dh.id);
+ }
+ });
+ }
+ if (hosts.length > 0) {
+ this.log.debug(hosts.length, 'Hosts highlighted');
+ hosts.forEach((hh) => {
+ const hostComponent: HostNodeSvgComponent = this.hosts.find((h) => h.host.id === hh.id );
+ if (hostComponent) {
+ hostComponent.ngOnChanges(
+ {'hostHighlight': new SimpleChange(<Host>{}, hh, true)}
+ );
+ this.log.debug('Highlighting host', hostComponent.host.id);
+ }
+ });
+ }
+ if (links.length > 0) {
+ this.log.debug(links.length, 'Links highlighted');
+ links.forEach((lh) => {
+ const linkComponent: LinkSvgComponent = this.links.find((l) => l.link.id === lh.id );
+ if (linkComponent) { // A link might not be present is hosts viewing is switched off
+ linkComponent.ngOnChanges(
+ {'linkHighlight': new SimpleChange(<LinkHighlight>{}, lh, true)}
+ );
+ // this.log.debug('Highlighting link', linkComponent.link.id, lh.css, lh.label);
+ }
+ });
+ }
+ }
+
+ /**
+ * As nodes are dragged around the graph, their new location should be sent
+ * back to server
+ * @param klass The class of node e.g. 'host' or 'device'
+ * @param id - the ID of the node
+ * @param newLocation - the new Location of the node
+ */
+ nodeMoved(klass: string, id: string, newLocation: MetaUi) {
+ this.wss.sendEvent('updateMeta', <UpdateMeta>{
+ id: id,
+ class: klass,
+ memento: newLocation
+ });
+ this.log.debug(klass, id, 'has been moved to', newLocation);
+ }
+
+ resetNodeLocations() {
+ this.devices.forEach((d) => {
+ d.resetNodeLocation();
+ });
+ }
+}
+
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.spec.ts
new file mode 100644
index 0000000..bdcd5c5
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.spec.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ForceDirectedGraph, Options} from './force-directed-graph';
+import {Node} from './node';
+import {Link} from './link';
+import {LogService} from 'gui2-fw-lib';
+import {TestBed} from '@angular/core/testing';
+
+export class TestNode extends Node {
+ constructor(id: string) {
+ super(id);
+ }
+}
+
+export class TestLink extends Link {
+ constructor(source: Node, target: Node) {
+ super(source, target);
+ }
+}
+
+/**
+ * ONOS GUI -- ForceDirectedGraph - Unit Tests
+ */
+describe('ForceDirectedGraph', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let fdg: ForceDirectedGraph;
+ const options: Options = {width: 1000, height: 1000};
+
+ beforeEach(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ const nodes: Node[] = [];
+ const links: Link[] = [];
+ fdg = new ForceDirectedGraph(options, logSpy);
+
+ for (let i = 0; i < 10; i++) {
+ const newNode: TestNode = new TestNode('id' + i);
+ nodes.push(newNode);
+ }
+ for (let j = 1; j < 10; j++) {
+ const newLink = new TestLink(nodes[0], nodes[j]);
+ links.push(newLink);
+ }
+ fdg.nodes = nodes;
+ fdg.links = links;
+ fdg.initSimulation(options);
+ fdg.initNodes();
+ logServiceSpy = TestBed.get(LogService);
+ });
+
+ afterEach(() => {
+ fdg.stopSimulation();
+ fdg.nodes = [];
+ fdg.links = [];
+ fdg.initSimulation(options);
+ });
+
+ it('should be created', () => {
+ expect(fdg).toBeTruthy();
+ });
+
+ it('should have simulation', () => {
+ expect(fdg.simulation).toBeTruthy();
+ });
+
+ it('should have 10 nodes', () => {
+ expect(fdg.nodes.length).toEqual(10);
+ });
+
+ it('should have 10 links', () => {
+ expect(fdg.links.length).toEqual(9);
+ });
+
+ // TODO fix these up to listen for tick
+ // it('nodes should not be at zero', () => {
+ // expect(nodes[0].x).toBeGreaterThan(0);
+ // });
+ // it('ticker should emit', () => {
+ // let tickMe = jasmine.createSpy("tickMe() spy");
+ // fdg.ticker.subscribe((simulation) => tickMe());
+ // expect(tickMe).toHaveBeenCalled();
+ // });
+
+ // it('init links chould be called ', () => {
+ // spyOn(fdg, 'initLinks');
+ // // expect(fdg).toBeTruthy();
+ // fdg.initSimulation(options);
+ // expect(fdg.initLinks).toHaveBeenCalled();
+ // });
+
+ it ('throws error on no options', () => {
+ expect(fdg.initSimulation).toThrowError('missing options when initializing simulation');
+ });
+
+
+
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.ts
new file mode 100644
index 0000000..24dd029
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/force-directed-graph.ts
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { EventEmitter } from '@angular/core';
+import { Link } from './link';
+import { Node } from './node';
+import * as d3 from 'd3-force';
+import {LogService} from 'gui2-fw-lib';
+
+const FORCES = {
+ LINKS: 1 / 50,
+ COLLISION: 1,
+ GRAVITY: 0.4,
+ FRICTION: 0.7
+};
+
+const CHARGES = {
+ device: -80,
+ host: -200,
+ region: -80,
+ _def_: -120
+};
+
+const LINK_DISTANCE = {
+ // note: key is link.type
+ direct: 100,
+ optical: 120,
+ UiEdgeLink: 100,
+ _def_: 50,
+};
+
+const LINK_STRENGTH = {
+ // note: key is link.type
+ // range: {0.0 ... 1.0}
+ _def_: 0.1
+};
+
+export interface Options {
+ width: number;
+ height: number;
+}
+
+/**
+ * The inspiration for this approach comes from
+ * https://medium.com/netscape/visualizing-data-with-angular-and-d3-209dde784aeb
+ */
+export class ForceDirectedGraph {
+ public ticker: EventEmitter<d3.Simulation<Node, Link>> = new EventEmitter();
+ public simulation: d3.Simulation<any, any>;
+
+ public nodes: Node[] = [];
+ public links: Link[] = [];
+
+ constructor(options: Options, public log: LogService) {
+ this.initSimulation(options);
+ }
+
+ initNodes() {
+ if (!this.simulation) {
+ throw new Error('simulation was not initialized yet');
+ }
+
+ this.simulation.nodes(this.nodes);
+ }
+
+ initLinks() {
+ if (!this.simulation) {
+ throw new Error('simulation was not initialized yet');
+ }
+
+ // Initializing the links force simulation
+ this.simulation.force('links',
+ d3.forceLink(this.links)
+ .strength(this.strength.bind(this))
+ .distance(this.distance.bind(this))
+ );
+ }
+
+ charges(node) {
+ const nodeType = node.nodeType;
+ return CHARGES[nodeType] || CHARGES._def_;
+ }
+
+ distance(node) {
+ const nodeType = node.nodeType;
+ return LINK_DISTANCE[nodeType] || LINK_DISTANCE._def_;
+ }
+
+ strength(node) {
+ const nodeType = node.nodeType;
+ return LINK_STRENGTH[nodeType] || LINK_STRENGTH._def_;
+ }
+
+ initSimulation(options: Options) {
+ if (!options || !options.width || !options.height) {
+ throw new Error('missing options when initializing simulation');
+ }
+
+ /** Creating the simulation */
+ if (!this.simulation) {
+ const ticker = this.ticker;
+
+ // Creating the force simulation and defining the charges
+ this.simulation = d3.forceSimulation()
+ .force('charge',
+ d3.forceManyBody().strength(this.charges.bind(this)))
+ // .distanceMin(100).distanceMax(500))
+ .force('gravity',
+ d3.forceManyBody().strength(FORCES.GRAVITY))
+ .force('friction',
+ d3.forceManyBody().strength(FORCES.FRICTION));
+
+ // Connecting the d3 ticker to an angular event emitter
+ this.simulation.on('tick', function () {
+ ticker.emit(this);
+ });
+
+ this.initNodes();
+ // this.initLinks();
+ }
+
+ /** Updating the central force of the simulation */
+ this.simulation.force('centers', d3.forceCenter(options.width / 2, options.height / 2));
+
+ /** Restarting the simulation internal timer */
+ this.simulation.restart();
+ }
+
+ stopSimulation() {
+ this.simulation.stop();
+ this.log.debug('Simulation stopped');
+ }
+
+ restartSimulation() {
+ this.simulation.restart();
+ this.log.debug('Simulation restarted');
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/index.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/index.ts
new file mode 100644
index 0000000..36fd2e7
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './node';
+export * from './link';
+export * from './regions';
+
+export * from './force-directed-graph';
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/link.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/link.ts
new file mode 100644
index 0000000..28f0b7c
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/link.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Node, UiElement} from './node';
+import * as d3 from 'd3';
+
+export enum LinkType {
+ UiRegionLink,
+ UiDeviceLink,
+ UiEdgeLink
+}
+
+/**
+ * model of the topo2CurrentRegion region rollup from Region below
+ *
+ */
+export interface RegionRollup {
+ id: string;
+ epA: string;
+ epB: string;
+ portA: string;
+ portB: string;
+ type: LinkType;
+}
+
+/**
+ * Implementing SimulationLinkDatum interface into our custom Link class
+ */
+export class Link implements UiElement, d3.SimulationLinkDatum<Node> {
+ // Optional - defining optional implementation properties - required for relevant typing assistance
+ index?: number;
+ id: string; // The id of the link in the format epA/portA~epB/portB
+ epA: string; // The name of the device or host at one end
+ epB: string; // The name of the device or host at the other end
+ portA: string; // The number of the port at one end
+ portB: string; // The number of the port at the other end
+ type: LinkType;
+
+ // Must - defining enforced implementation properties
+ source: Node;
+ target: Node;
+
+ public static deviceNameFromEp(ep: string): string {
+ if (ep !== undefined && ep.lastIndexOf('/') > 0) {
+ return ep.substr(0, ep.lastIndexOf('/'));
+ }
+ return ep;
+ }
+
+ constructor(source, target) {
+ this.source = source;
+ this.target = target;
+ }
+
+ linkTypeStr(): string {
+ return LinkType[this.type];
+ }
+}
+
+/**
+ * model of the topo2CurrentRegion region link from Region
+ */
+export class RegionLink extends Link {
+ rollup: RegionRollup[]; // Links in sub regions represented by this one link
+
+ constructor(type: LinkType, nodeA: Node, nodeB: Node) {
+ super(nodeA, nodeB);
+ this.type = type;
+ }
+}
+
+/**
+ * model of the highlights that are sent back from WebSocket when traffic is shown
+ */
+export interface LinkHighlight {
+ id: string;
+ css: string;
+ label: string;
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts
new file mode 100644
index 0000000..ce22e99
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/node.ts
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as d3 from 'd3';
+import {LocationType} from '../../backgroundsvg/backgroundsvg.component';
+import {LayerType, Location, NodeType, RegionProps} from './regions';
+import {MetaUi} from 'gui2-fw-lib';
+
+export interface UiElement {
+ index?: number;
+ id: string;
+}
+
+export namespace LabelToggle {
+ /**
+ * Toggle state for how device labels should be displayed
+ */
+ export enum Enum {
+ NONE,
+ ID,
+ NAME
+ }
+
+ /**
+ * Add the method 'next()' to the LabelToggle enum above
+ */
+ export function next(current: Enum) {
+ if (current === Enum.NONE) {
+ return Enum.ID;
+ } else if (current === Enum.ID) {
+ return Enum.NAME;
+ } else if (current === Enum.NAME) {
+ return Enum.NONE;
+ }
+ }
+}
+
+export namespace HostLabelToggle {
+ /**
+ * Toggle state for how host labels should be displayed
+ */
+ export enum Enum {
+ NONE,
+ NAME,
+ IP,
+ MAC
+ }
+
+ /**
+ * Add the method 'next()' to the HostLabelToggle enum above
+ */
+ export function next(current: Enum) {
+ if (current === Enum.NONE) {
+ return Enum.NAME;
+ } else if (current === Enum.NAME) {
+ return Enum.IP;
+ } else if (current === Enum.IP) {
+ return Enum.MAC;
+ } else if (current === Enum.MAC) {
+ return Enum.NONE;
+ }
+ }
+}
+
+export namespace GridDisplayToggle {
+ /**
+ * Toggle state for how the grid should be displayed
+ */
+ export enum Enum {
+ GRIDNONE,
+ GRID1000,
+ GRIDGEO,
+ GRIDBOTH
+ }
+
+ /**
+ * Add the method 'next()' to the GridDisplayToggle enum above
+ */
+ export function next(current: Enum) {
+ if (current === Enum.GRIDNONE) {
+ return Enum.GRID1000;
+ } else if (current === Enum.GRID1000) {
+ return Enum.GRIDGEO;
+ } else if (current === Enum.GRIDGEO) {
+ return Enum.GRIDBOTH;
+ } else if (current === Enum.GRIDBOTH) {
+ return Enum.GRIDNONE;
+ }
+ }
+}
+
+/**
+ * model of the topo2CurrentRegion device props from Device below
+ */
+export interface DeviceProps {
+ latitude: number;
+ longitude: number;
+ name: string;
+ locType: LocationType;
+ uiType: string;
+ channelId: string;
+ managementAddress: string;
+ protocol: string;
+}
+
+export interface HostProps {
+ gridX: number;
+ gridY: number;
+ latitude: number;
+ longitude: number;
+ locType: LocationType;
+ name: string;
+}
+
+/**
+ * Implementing SimulationNodeDatum interface into our custom Node class
+ */
+export abstract class Node implements UiElement, d3.SimulationNodeDatum {
+ // Optional - defining optional implementation properties - required for relevant typing assistance
+ index?: number;
+ x: number;
+ y: number;
+ vx?: number;
+ vy?: number;
+ fx?: number | null;
+ fy?: number | null;
+ nodeType: NodeType;
+ id: string;
+
+ protected constructor(id) {
+ this.id = id;
+ this.x = 0;
+ this.y = 0;
+ }
+}
+
+/**
+ * model of the topo2CurrentRegion device from Region below
+ */
+export class Device extends Node {
+ id: string;
+ layer: LayerType;
+ location: Location;
+ metaUi: MetaUi;
+ master: string;
+ online: boolean;
+ props: DeviceProps;
+ type: string;
+
+ constructor(id: string) {
+ super(id);
+ }
+}
+
+/**
+ * Model of the ONOS Host element in the topology
+ */
+export class Host extends Node {
+ configured: boolean;
+ id: string;
+ ips: string[];
+ layer: LayerType;
+ props: HostProps;
+
+ constructor(id: string) {
+ super(id);
+ }
+}
+
+
+/**
+ * model of the topo2CurrentRegion subregion from Region below
+ */
+export class SubRegion extends Node {
+ id: string;
+ location: Location;
+ nDevs: number;
+ nHosts: number;
+ name: string;
+ props: RegionProps;
+
+ constructor(id: string) {
+ super(id);
+ }
+}
+
+/**
+ * Enumerated values for topology update event types
+ */
+export enum ModelEventType {
+ HOST_ADDED_OR_UPDATED,
+ LINK_ADDED_OR_UPDATED,
+ DEVICE_ADDED_OR_UPDATED,
+ DEVICE_REMOVED,
+ HOST_REMOVED
+}
+
+/**
+ * Enumerated values for topology update event memo field
+ */
+export enum ModelEventMemo {
+ ADDED = 'added',
+ REMOVED = 'removed',
+ UPDATED = 'updated'
+}
+
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/regions.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/regions.ts
new file mode 100644
index 0000000..3c1894b
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/models/regions.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Enum of the topo2CurrentRegion node type from SubRegion below
+ */
+import {LocationType} from '../../backgroundsvg/backgroundsvg.component';
+import {Device, Host, SubRegion} from './node';
+import {RegionLink} from './link';
+
+export enum NodeType {
+ REGION = 'region',
+ DEVICE = 'device',
+ HOST = 'host',
+}
+
+/**
+ * Enum of the topo2CurrentRegion layerOrder from Region below
+ */
+export enum LayerType {
+ LAYER_OPTICAL = 'opt',
+ LAYER_PACKET = 'pkt',
+ LAYER_DEFAULT = 'def'
+}
+
+/**
+ * model of the topo2CurrentRegion location from SubRegion below
+ */
+export interface Location {
+ locType: LocationType;
+ latOrY: number;
+ longOrX: number;
+}
+
+/**
+ * model of the topo2CurrentRegion props from SubRegion below
+ */
+export interface RegionProps {
+ latitude: number;
+ longitude: number;
+ name: string;
+ peerLocations: string;
+}
+
+/**
+ * model of the topo2CurrentRegion WebSocket response
+ *
+ * The Devices are in a 2D array - 1st order is layer type, 2nd order is
+ * devices in that layer
+ */
+export interface Region {
+ note?: string;
+ id: string;
+ devices: Device[][];
+ hosts: Host[][];
+ links: RegionLink[];
+ layerOrder: LayerType[];
+ peerLocations?: Location[];
+ subregions: SubRegion[];
+}
+
+/**
+ * model of the topo2PeerRegions WebSocket response
+ */
+export interface Peer {
+ peers: SubRegion[];
+}
+
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
new file mode 100644
index 0000000..204b85c
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.css
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ ONOS GUI -- Topology View (forces device visual) -- CSS file
+ */
+g.node.device rect {
+ fill: #f0f0f070;
+}
+g.node.device text {
+ fill: #bbb;
+}
+g.node.device use {
+ fill: #777;
+}
+
+
+g.node.device.online rect {
+ fill: #fafafad0;
+}
+g.node.device.online text {
+ fill: #3c3a3a;
+}
+g.node.device.online use {
+ /* NOTE: this gets overridden programatically */
+ fill: #ffffff;
+}
+
+g.node.selected .node-container {
+ stroke-width: 2.0;
+ stroke: #009fdb;
+}
+
+g.node.hovered .node-container {
+ stroke-width: 2.0;
+ stroke: #454545;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
new file mode 100644
index 0000000..399955c
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.html
@@ -0,0 +1,91 @@
+<!--
+~ Copyright 2019-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<svg:defs xmlns:svg="http://www.w3.org/2000/svg">
+ <!-- Template explanation: Define an SVG Filter that in
+ line 0) creates a box big enough to accommodate the drop shadow
+ line 1) render the target object in to a bit map and apply a blur to it
+ based on its alpha channel
+ line 2) take that blurred layer and shift it down and to the right by 4
+ line 3) Merge this blurred and shifted layer and overlay it with the
+ original target object
+ -->
+ <svg:filter id="drop-shadow" x="-25%" y="-25%" width="200%" height="200%">
+ <svg:feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur" />
+ <svg:feOffset in="blur" dx="4" dy="4" result="offsetBlur"/>
+ <svg:feMerge >
+ <svg:feMergeNode in="offsetBlur" />
+ <svg:feMergeNode in="SourceGraphic" />
+ </svg:feMerge>
+ </svg:filter>
+ <!-- Template explanation: Define a colour gradient that can be used in icons -->
+ <svg:linearGradient id="diagonal_blue" x1="0%" y1="0%" x2="100%" y2="100%">
+ <svg:stop offset= "0%" style="stop-color: #7fabdb;" />
+ <svg:stop offset= "100%" style="stop-color: #5b99d2;" />
+ </svg:linearGradient>
+</svg:defs>
+<!-- Template explanation: Creates an SVG Group and in
+ line 1) transform it to the position calculated by the d3 force graph engine
+ and scale it inversely to the zoom level
+ line 2) Give it various CSS styles depending on attributes
+ line 3) When it is clicked, call the method that toggles the selection and
+ emits an event.
+ Other child objects have their own description
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+ [attr.transform]="'translate(' + device?.x + ',' + device?.y + '), scale(' + scale + ')'"
+ [ngClass]="['node', 'device', device.online?'online':'', selected?'selected':'']"
+ (click)="toggleSelected(device)">
+ <svg:desc>Device {{device.id}}</svg:desc>
+ <!-- Template explanation: Creates an SVG Rectangle and in
+ line 1) set a css style and shift so that it's centred
+ line 2) set the initial width and height - width changes with label
+ line 3) link to the animation 'deviceLabelToggle', pass in to it a width
+ calculated from the width of the text, and additional padding at the end
+ line 4) Apply the filter defined above to this rectangle (even as its
+ width changes
+ -->
+ <svg:rect
+ class="node-container" x="-18" y="-18"
+ width="36" height="36"
+ [@deviceLabelToggle]="{ value: labelToggle, params: {txtWidth: (36 + labelTextLen() * 1.1)+'px' }}"
+ filter= "url(#drop-shadow)">
+ </svg:rect>
+ <!-- Template explanation: Creates an SVG Rectangle slightly smaller and
+ overlaid on the above. This is the blue box, and its width and height does
+ not change
+ -->
+ <svg:rect x="-16" y="-16" width="32" height="32" style="fill: url(#diagonal_blue)">
+ </svg:rect>
+ <!-- Template explanation: Creates an SVG Text element and in
+ line 1) make it left aligned and slightly down and to the right of the last rect
+ line 2) set its text length to be the calculated value - see that function
+ line 3) because of kerning the actual text might be shorter or longer than
+ the pre-calculated value - if so change the spacing between the letters
+ (and not the letter width to compensate)
+ line 4) link to the animation deviceLabelToggleTxt, so that the text appears
+ in gently
+ line 5) The text will be one of 3 values - blank, the id or the name
+ -->
+ <svg:text
+ text-anchor="start" y="0.3em" x="22"
+ [attr.textLength]= "labelTextLen()"
+ lengthAdjust= "spacing"
+ [@deviceLabelToggleTxt]="labelToggle">
+ {{ labelToggle == 0 ? '': labelToggle == 1 ? device.id:device.props.name }}
+ </svg:text>
+ <svg:use [attr.xlink:href]="'#' + deviceIcon()" width="36" height="36" x="-18" y="-18">
+ </svg:use>
+</svg:g>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
new file mode 100644
index 0000000..b490605
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.spec.ts
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DeviceNodeSvgComponent } from './devicenodesvg.component';
+import {IconService, LogService} from 'gui2-fw-lib';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {ChangeDetectorRef} from '@angular/core';
+import {Device} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+class MockIconService {
+ loadIconDef() { }
+}
+
+describe('DeviceNodeSvgComponent', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: DeviceNodeSvgComponent;
+ let fixture: ComponentFixture<DeviceNodeSvgComponent>;
+ let ar: MockActivatedRoute;
+ let testDevice: Device;
+
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+ testDevice = new Device('test:1');
+ testDevice.online = true;
+
+ TestBed.configureTestingModule({
+ imports: [ BrowserAnimationsModule ],
+ declarations: [ DeviceNodeSvgComponent ],
+ providers: [
+ { provide: LogService, useValue: logSpy },
+ { provide: ActivatedRoute, useValue: ar },
+ { provide: ChangeDetectorRef, useClass: ChangeDetectorRef },
+ { provide: IconService, useClass: MockIconService }
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DeviceNodeSvgComponent);
+ component = fixture.componentInstance;
+ component.device = testDevice;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
new file mode 100644
index 0000000..ce6bd46
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component.ts
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges, Output,
+ SimpleChanges,
+} from '@angular/core';
+import {Device, LabelToggle, UiElement} from '../../models';
+import {IconService, LocMeta, LogService, MetaUi, ZoomUtils} from 'gui2-fw-lib';
+import {NodeVisual} from '../nodevisual';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+import {LocationType} from '../../../backgroundsvg/backgroundsvg.component';
+
+/**
+ * The Device node in the force graph
+ *
+ * Note: here the selector is given square brackets [] so that it can be
+ * inserted in SVG element like a directive
+ */
+@Component({
+ selector: '[onos-devicenodesvg]',
+ templateUrl: './devicenodesvg.component.html',
+ styleUrls: ['./devicenodesvg.component.css'],
+ changeDetection: ChangeDetectionStrategy.Default,
+ animations: [
+ trigger('deviceLabelToggle', [
+ state('0', style({ // none
+ width: '36px',
+ })),
+ state('1, 2', // id
+ style({ width: '{{ txtWidth }}'}),
+ { params: {'txtWidth': '36px'}}
+ ), // default
+ transition('0 => 1', animate('250ms ease-in')),
+ transition('1 => 2', animate('250ms ease-in')),
+ transition('* => 0', animate('250ms ease-out'))
+ ]),
+ trigger('deviceLabelToggleTxt', [
+ state('0', style( {
+ opacity: 0,
+ })),
+ state( '1,2', style({
+ opacity: 1.0
+ })),
+ transition('0 => 1', animate('250ms ease-in')),
+ transition('* => 0', animate('250ms ease-out'))
+ ])
+ ]
+})
+export class DeviceNodeSvgComponent extends NodeVisual implements OnChanges {
+ @Input() device: Device;
+ @Input() scale: number = 1.0;
+ @Input() labelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
+ @Output() selectedEvent = new EventEmitter<UiElement>();
+ textWidth: number = 36;
+ constructor(
+ protected log: LogService,
+ private is: IconService,
+ private ref: ChangeDetectorRef
+ ) {
+ super();
+ }
+
+ /**
+ * Called by parent (forcesvg) when a change happens
+ *
+ * There is a difficulty in passing the SVG text object to the animation
+ * directly, to get its width, so we capture it here and update textWidth
+ * local variable here and use it in the animation
+ */
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['device']) {
+ if (!this.device.x) {
+ this.device.x = 0;
+ this.device.y = 0;
+ }
+ }
+ this.ref.markForCheck();
+ }
+
+ /**
+ * Calculate the text length in advance as well as possible
+ *
+ * The length of SVG text cannot be exactly estimated, because depending on
+ * the letters kerning might mean that it is shorter or longer than expected
+ *
+ * This takes the approach of 8px width per letter of this size, that on average
+ * evens out over words. A word like 'ilj' will be much shorter than 'wm0'
+ * because of kerning
+ *
+ *
+ * In addition in the template, the <svg:text> properties
+ * textLength and lengthAdjust ensure that the text becomes long with extra
+ * wide spacing created as necessary.
+ *
+ * Other approaches like getBBox() of the text
+ */
+ labelTextLen() {
+ if (this.labelToggle === 1) {
+ return this.device.id.length * 8;
+ } else if (this.labelToggle === 2 && this.device &&
+ this.device.props.name && this.device.props.name.trim().length > 0) {
+ return this.device.props.name.length * 8;
+ } else {
+ return 0;
+ }
+ }
+
+ deviceIcon(): string {
+ if (this.device.props && this.device.props.uiType) {
+ this.is.loadIconDef(this.device.props.uiType);
+ return this.device.props.uiType;
+ } else {
+ return 'm_' + this.device.type;
+ }
+ }
+
+ resetNodeLocation(): void {
+ this.log.debug('Resetting device', this.device.id, this.device.type);
+ let origLoc: MetaUi;
+
+ if (!this.device.location || this.device.location.locType === LocationType.NONE) {
+ // No location - nothing to do
+ return;
+ } else if (this.device.location.locType === LocationType.GEO) {
+ origLoc = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+ lng: this.device.location.longOrX,
+ lat: this.device.location.latOrY
+ });
+ } else if (this.device.location.locType === LocationType.GRID) {
+ origLoc = ZoomUtils.convertXYtoGeo(
+ this.device.location.longOrX, this.device.location.latOrY);
+ }
+ this.device.metaUi = origLoc;
+ this.device['fx'] = origLoc.x;
+ this.device['fy'] = origLoc.y;
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.css
new file mode 100644
index 0000000..92a114f
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.css
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ ONOS GUI -- Topology View (forces host visual) -- CSS file
+ */
+.node.host text {
+ stroke: none;
+ font-size: 13px;
+ fill: #846;
+}
+
+.node.host circle {
+ stroke: #a3a596;
+ fill: #e0dfd6;
+}
+
+.node.host.selected > circle {
+ stroke-width: 2.0;
+ stroke: #009fdb;
+}
+
+.node.host use {
+ fill: #3c3a3a;
+}
+
+.node.host rect {
+ fill: #ffffff;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
new file mode 100644
index 0000000..ccce65a
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.html
@@ -0,0 +1,70 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<svg:defs xmlns:svg="http://www.w3.org/2000/svg">
+ <!-- Template explanation: Define an SVG Filter that in
+ line 1) render the target object in to a bit map and apply a blur to it
+ based on its alpha channel
+ line 2) take that blurred layer and shift it down and to the right by 4
+ line 3) Merge this blurred and shifted layer and overlay it with the
+ original target object
+ -->
+ <svg:filter id="drop-shadow-host" x="-25%" y="-25%" width="200%" height="200%">
+ <svg:feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur" />
+ <svg:feOffset in="blur" dx="4" dy="4" result="offsetBlur"/>
+ <svg:feMerge >
+ <svg:feMergeNode in="offsetBlur" />
+ <svg:feMergeNode in="SourceGraphic" />
+ </svg:feMerge>
+ </svg:filter>
+ <svg:radialGradient id="three_stops_radial">
+ <svg:stop offset= "0%" style="stop-color: #e3e5d6;" />
+ <svg:stop offset= "70%" style="stop-color: #c3c5b6;" />
+ <svg:stop offset="100%" style="stop-color: #a3a596;" />
+ </svg:radialGradient>
+</svg:defs>
+<!-- Template explanation: Creates an SVG Group and in
+ line 1) transform it to the position calculated by the d3 force graph engine
+ line 2) Give it various CSS styles depending on attributes
+ line 3) When it is clicked, call the method that toggles the selection and
+ emits an event.
+ Other child objects have their own description
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+ [attr.transform]="'translate(' + host?.x + ',' + host?.y + '), scale(' + scale + ')'"
+ [ngClass]="['node', 'host', 'endstation', 'fixed', selected?'selected':'', 'hovered']"
+ (click)="toggleSelected(host)">
+ <svg:desc>Host {{host.id}}</svg:desc>
+ <!-- Template explanation: Creates an SVG Circle and in
+ line 1) Apply the drop shadow defined above to this circle
+ line 2) Apply the radial gradient defined above to the circle
+ -->
+ <svg:circle r="15"
+ filter="url(#drop-shadow-host)"
+ style="fill: url(#three_stops_radial)">
+ </svg:circle>
+ <svg:use xlink:href="#m_endstation" width="22.5" height="22.5" x="-11.25" y="-11.25">
+ </svg:use>
+ <!-- Template explanation: Creates an SVG Text
+ line 1) if the labelToggle is not 0
+ line 2) shift it below the circle, and have it centred with the circle
+ line 3) apply a scale and call on the hostName(0 method to get the
+ displayed value
+ -->
+ <svg:text
+ *ngIf="labelToggle != 0"
+ dy="30" text-anchor="middle"
+ >{{hostName()}}</svg:text>
+</svg:g>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts
new file mode 100644
index 0000000..e3efb3f
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.spec.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HostNodeSvgComponent } from './hostnodesvg.component';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {LogService} from 'gui2-fw-lib';
+import {Host} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {ChangeDetectorRef} from '@angular/core';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+describe('HostNodeSvgComponent', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: HostNodeSvgComponent;
+ let fixture: ComponentFixture<HostNodeSvgComponent>;
+ let ar: MockActivatedRoute;
+ let testHost: Host;
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+ testHost = new Host('host:1');
+ testHost.ips = ['10.205.86.123', '192.168.56.10'];
+
+ TestBed.configureTestingModule({
+ imports: [ BrowserAnimationsModule ],
+ declarations: [ HostNodeSvgComponent ],
+ providers: [
+ { provide: LogService, useValue: logSpy },
+ { provide: ActivatedRoute, useValue: ar },
+ { provide: ChangeDetectorRef, useClass: ChangeDetectorRef }
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HostNodeSvgComponent);
+ component = fixture.componentInstance;
+ component.host = testHost;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.ts
new file mode 100644
index 0000000..26c105a
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges,
+ Output,
+ SimpleChanges
+} from '@angular/core';
+import {Host, HostLabelToggle, Node} from '../../models';
+import {LogService} from 'gui2-fw-lib';
+import {NodeVisual} from '../nodevisual';
+
+/**
+ * The Host node in the force graph
+ *
+ * Note: here the selector is given square brackets [] so that it can be
+ * inserted in SVG element like a directive
+ */
+@Component({
+ selector: '[onos-hostnodesvg]',
+ templateUrl: './hostnodesvg.component.html',
+ styleUrls: ['./hostnodesvg.component.css']
+})
+export class HostNodeSvgComponent extends NodeVisual implements OnChanges {
+ @Input() host: Host;
+ @Input() scale: number = 1.0;
+ @Input() labelToggle: HostLabelToggle.Enum = HostLabelToggle.Enum.IP;
+ @Output() selectedEvent = new EventEmitter<Node>();
+
+ constructor(
+ protected log: LogService
+ ) {
+ super();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (!this.host.x) {
+ this.host.x = 0;
+ this.host.y = 0;
+ }
+ }
+
+ hostName(): string {
+ if (this.host === undefined) {
+ return undefined;
+ } else if (this.labelToggle === HostLabelToggle.Enum.IP) {
+ return this.host.ips.join(',');
+ } else if (this.labelToggle === HostLabelToggle.Enum.MAC) {
+ return this.host.id;
+ } else {
+ return this.host.id; // Todo - replace with a friendly name
+ }
+
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.css
new file mode 100644
index 0000000..58548c4
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.css
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ ONOS GUI -- Topology View (forces link svg) -- CSS file
+ */
+/* --- Topo Links --- */
+line {
+ stroke: #888888;
+ stroke-width: 2px;
+}
+
+.link {
+ opacity: .9;
+}
+
+.link.selected {
+ stroke: #009fdb;
+}
+.link.enhanced {
+ stroke: #009fdb;
+ stroke-width: 4px;
+ cursor: pointer;
+}
+
+.link.inactive {
+ opacity: .5;
+ stroke-dasharray: 4 2;
+}
+/* TODO: Review for not-permitted links */
+.link.not-permitted {
+ stroke: rgb(255,0,0);
+ stroke-dasharray: 8 4;
+}
+
+.link.secondary {
+ stroke: rgba(0,153,51,0.5);
+}
+
+.link.secondary.port-traffic-green {
+ stroke: rgb(0,153,51);
+}
+
+.link.secondary.port-traffic-yellow {
+ stroke: rgb(128,145,27);
+}
+
+.link.secondary.port-traffic-orange {
+ stroke: rgb(255, 137, 3);
+}
+
+.link.secondary.port-traffic-red {
+ stroke: rgb(183, 30, 21);
+}
+
+/* Port traffic color visualization for Kbps, Mbps, and Gbps */
+
+.link.secondary.port-traffic-Kbps {
+ stroke: rgb(0,153,51);
+}
+
+.link.secondary.port-traffic-Mbps {
+ stroke: rgb(128,145,27);
+}
+
+.link.secondary.port-traffic-Gbps {
+ stroke: rgb(255, 137, 3);
+}
+
+.link.secondary.port-traffic-Gbps-choked {
+ stroke: rgb(183, 30, 21);
+}
+
+.link.animated {
+ stroke-dasharray: 8 5;
+ animation: ants 5s infinite linear;
+ /* below line could be added via Javascript, based on path, if we cared
+ * enough about the direction of ant-flow
+ */
+ /*animation-direction: reverse;*/
+}
+@keyframes ants {
+ from {
+ stroke-dashoffset: 0;
+ }
+ to {
+ stroke-dashoffset: 400;
+ }
+}
+
+.link.primary {
+ stroke-width: 4px;
+ stroke: #ffA300;
+}
+
+.link.secondary.optical {
+ stroke-width: 4px;
+ stroke: rgba(128,64,255,0.5);
+}
+
+.link.primary.optical {
+ stroke-width: 6px;
+ stroke: #74f;
+}
+
+/* Link Labels */
+.linkLabel rect {
+ stroke: none;
+ fill: #ffffff;
+}
+
+.linkLabel text {
+ fill: #444;
+ text-anchor: middle;
+}
+
+
+/* Port Labels */
+.portLabel rect {
+ stroke: #a3a596;
+ fill: #ffffff;
+}
+
+.portLabel {
+ fill: #444;
+ alignment-baseline: middle;
+ dominant-baseline: middle;
+}
+
+/* Number of Links Labels */
+
+
+#ov-topo2 text.numLinkText {
+ fill: #444;
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.html
new file mode 100644
index 0000000..c856763
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.html
@@ -0,0 +1,100 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<svg:defs xmlns:svg="http://www.w3.org/2000/svg">
+ <svg:filter id="glow">
+ <svg:feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0.9 0 0 0 0 0.9 0 0 0 0 1 0" />
+ <svg:feGaussianBlur stdDeviation="2.5" result="coloredBlur" />
+ <svg:feMerge>
+ <svg:feMergeNode in="coloredBlur" />
+ <svg:feMergeNode in="SourceGraphic"/>
+ </svg:feMerge>
+ </svg:filter>
+</svg:defs>
+<!-- Template explanation: Creates an SVG Line and in
+ line 1) transform end A to the position calculated by the d3 force graph engine
+ line 2) transform end B to the position calculated by the d3 force graph engine
+ line 3) Give it various CSS styles depending on attributes
+ ling 4) Change the line width depending on the scale
+ line 4) When it is clicked, call the method that toggles the selection and
+ emits an event.
+ line 5) When the mouse is moved over call on enhance() function. This will
+ flash up the port labels, and display the link in blue for 1 second
+ Other child objects have their own description
+-->
+<svg:line xmlns:svg="http://www.w3.org/2000/svg"
+ [attr.x1]="link.source?.x" [attr.y1]="link.source?.y"
+ [attr.x2]="link.target?.x" [attr.y2]="link.target?.y"
+ [ngClass]="['link', selected?'selected':'', enhanced?'enhanced':'', highlighted]"
+ [ngStyle]="{'stroke-width': (enhanced ? 4 : 2) * scale + 'px'}"
+ (click)="toggleSelected(link)"
+ (mouseover)="enhance()"
+ [attr.filter]="highlighted?'url(#glow)':'none'">
+</svg:line>
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+ [ngClass]="['linkLabel']"
+ [attr.transform]="'scale(' + scale + ')'">
+ <!-- Template explanation: Creates SVG Text in the middle of the link to
+ show traffic and in:
+ line 1) Performs the animation 'linkLabelVisible' whenever the isHighlighted
+ boolean value changes
+ line 2 & 3) Sets the text at half way between the 2 end points of the line
+ Note: we do not use an *ngIf to enable or disable this, because that would
+ cause the fade out of the text to not work
+ -->
+ <svg:text xmlns:svg="http://www.w3.org/2000/svg"
+ [@linkLabelVisible]="isHighlighted"
+ [attr.x]="link.source?.x + (link.target?.x - link.source?.x)/2"
+ [attr.y]="link.source?.y + (link.target?.y - link.source?.y)/2"
+ >{{ label }}</svg:text>
+</svg:g>
+<!-- Template explanation: Creates an SVG Group if
+ line 1) 'enhanced' is active and port text exists
+ line 2) assigns classes to it
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+ *ngIf="enhanced && link.portA"
+ class="portLabel"
+ [attr.transform]="'translate(' + labelPosSrc.x + ',' + labelPosSrc.y + '),scale(' + scale + ')'">
+ <!-- Template explanation: Creates an SVG Rectangle and in
+ line 1) transform end A to the position calculated by the d3 force graph engine
+ line 2) assigns classes to it
+ -->
+ <svg:rect
+ [attr.x]="2 - textLength(link.portA)/2" y="-8"
+ [attr.width]="4 + textLength(link.portA)" height="16" >
+ </svg:rect>
+ <!-- Template explanation: Creates SVG Text and in
+ line 1) transform it to the position calculated by the method labelPosSrc()
+ line 2) centre aligns it
+ line 3) ensures that the text fills the rectangle by adjusting spacing
+ -->
+ <svg:text y="2" text-anchor="middle"
+ [attr.textLength]= "textLength(link.portA)" lengthAdjust="spacing"
+ >{{ link.portA }}</svg:text>
+</svg:g>
+<!-- A repeat of the above, but for the other end of the line -->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg"
+ *ngIf="enhanced && link.portB"
+ class="portLabel"
+ [attr.transform]="'translate(' + labelPosTgt.x + ',' + labelPosTgt.y + '),scale(' + scale + ')'">
+ <svg:rect
+ [attr.x]="2 - textLength(link.portB)/2" y="-8"
+ [attr.width]="4 + textLength(link.portB)" height="16">
+ </svg:rect>
+ <svg:text x="2" y="2" text-anchor="middle"
+ [attr.textLength]= "textLength(link.portB)" lengthAdjust="spacing"
+ >{{ link.portB }}</svg:text>
+</svg:g>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts
new file mode 100644
index 0000000..7626ff5
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.spec.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LinkSvgComponent } from './linksvg.component';
+import {LogService} from 'gui2-fw-lib';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {Device, Link, RegionLink, LinkType} from '../../models';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+describe('LinkVisualComponent', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: LinkSvgComponent;
+ let fixture: ComponentFixture<LinkSvgComponent>;
+ let ar: MockActivatedRoute;
+ let testLink: Link;
+ let testDeviceA: Device;
+ let testDeviceB: Device;
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+ testDeviceA = new Device('test:A');
+ testDeviceA.online = true;
+
+ testDeviceB = new Device('test:B');
+ testDeviceB.online = true;
+
+ testLink = new RegionLink(LinkType.UiDeviceLink, testDeviceA, testDeviceB);
+ testLink.id = 'test:A/1-test:B/1';
+
+ TestBed.configureTestingModule({
+ imports: [ BrowserAnimationsModule ],
+ declarations: [ LinkSvgComponent ],
+ providers: [
+ { provide: LogService, useValue: logSpy },
+ { provide: ActivatedRoute, useValue: ar },
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LinkSvgComponent);
+ component = fixture.componentInstance;
+ component.link = testLink;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.ts
new file mode 100644
index 0000000..eab533f
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/linksvg/linksvg.component.ts
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ ChangeDetectorRef,
+ Component, EventEmitter,
+ Input, OnChanges, Output, SimpleChanges,
+} from '@angular/core';
+import {Link, LinkHighlight, UiElement} from '../../models';
+import {LogService} from 'gui2-fw-lib';
+import {NodeVisual} from '../nodevisual';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+
+interface Point {
+ x: number;
+ y: number;
+}
+
+@Component({
+ selector: '[onos-linksvg]',
+ templateUrl: './linksvg.component.html',
+ styleUrls: ['./linksvg.component.css'],
+ animations: [
+ trigger('linkLabelVisible', [
+ state('true', style( {
+ opacity: 1.0,
+ })),
+ state( 'false', style({
+ opacity: 0
+ })),
+ transition('false => true', animate('500ms ease-in')),
+ transition('true => false', animate('1000ms ease-out'))
+ ])
+ ]
+})
+export class LinkSvgComponent extends NodeVisual implements OnChanges {
+ @Input() link: Link;
+ @Input() highlighted: string = '';
+ @Input() highlightsEnabled: boolean = true;
+ @Input() label: string;
+ @Input() scale = 1.0;
+ isHighlighted: boolean = false;
+ @Output() selectedEvent = new EventEmitter<UiElement>();
+ @Output() enhancedEvent = new EventEmitter<Link>();
+ enhanced: boolean = false;
+ labelPosSrc: Point = {x: 0, y: 0};
+ labelPosTgt: Point = {x: 0, y: 0};
+
+ constructor(
+ protected log: LogService,
+ private ref: ChangeDetectorRef
+ ) {
+ super();
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['linkHighlight']) {
+ const hl: LinkHighlight = changes['linkHighlight'].currentValue;
+ this.highlighted = hl.css;
+ this.label = hl.label;
+ this.isHighlighted = true;
+ setTimeout(() => {
+ this.isHighlighted = false;
+ this.highlighted = '';
+ this.ref.markForCheck();
+ }, 4990);
+
+ }
+
+ this.ref.markForCheck();
+ }
+
+ enhance() {
+ if (!this.highlightsEnabled) {
+ return;
+ }
+ this.enhancedEvent.emit(this.link);
+ this.enhanced = true;
+ this.repositionLabels();
+ setTimeout(() => {
+ this.enhanced = false;
+ this.ref.markForCheck();
+ }, 1000);
+ }
+
+ /**
+ * We want to place the label for the port about 40 px from the node.
+ * If the distance between the nodes is less than 100, then just place the
+ * label 1/3 of the way from the node
+ */
+ repositionLabels(): void {
+ const x1: number = this.link.source.x;
+ const y1: number = this.link.source.y;
+ const x2: number = this.link.target.x;
+ const y2: number = this.link.target.y;
+
+ const dist = Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2));
+ const offset = dist > 100 ? 40 : dist / 3;
+ this.labelPosSrc = <Point>{
+ x: x1 + (x2 - x1) * offset / dist,
+ y: y1 + (y2 - y1) * offset / dist
+ };
+
+ this.labelPosTgt = <Point>{
+ x: x2 - (x2 - x1) * offset / dist,
+ y: y2 - (y2 - y1) * offset / dist
+ };
+ }
+
+ /**
+ * For the 14pt font we are using, the average width seems to be about 8px
+ * @param text The string we want to calculate a width for
+ */
+ textLength(text: string) {
+ return text.length * 8;
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/nodevisual.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/nodevisual.ts
new file mode 100644
index 0000000..a41df62
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/nodevisual.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {EventEmitter} from '@angular/core';
+import {UiElement} from '../models';
+
+/**
+ * A base class for the Host and Device components
+ */
+export abstract class NodeVisual {
+ selected: boolean;
+ selectedEvent = new EventEmitter<UiElement>();
+
+ toggleSelected(uiElement: UiElement) {
+ this.selected = !this.selected;
+ if (this.selected) {
+ this.selectedEvent.emit(uiElement);
+ } else {
+ this.selectedEvent.emit();
+ }
+ }
+
+ deselect() {
+ this.selected = false;
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.css
new file mode 100644
index 0000000..87c23bd
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.css
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ ONOS GUI -- Topology View (forces subRegion visual) -- CSS file
+ */
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.html
new file mode 100644
index 0000000..5760634
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.html
@@ -0,0 +1,23 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" [attr.transform]="'translate(' + subRegion?.x + ',' + subRegion?.y + ')'">>
+ <svg:circle
+ cx="0"
+ cy="0"
+ r="5">
+ </svg:circle>
+ <svg:text>{{subRegion?.id}}</svg:text>
+</svg:g>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.spec.ts
new file mode 100644
index 0000000..d6f6446
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.spec.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SubRegionNodeSvgComponent } from './subregionnodesvg.component';
+import {SubRegion} from '../../models';
+
+describe('SubRegionNodeSvgComponent', () => {
+ let component: SubRegionNodeSvgComponent;
+ let fixture: ComponentFixture<SubRegionNodeSvgComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ SubRegionNodeSvgComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SubRegionNodeSvgComponent);
+ component = fixture.debugElement.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should create with an input', () => {
+ component.subRegion = new SubRegion('testId');
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.ts
new file mode 100644
index 0000000..5dd4736
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';
+import {SubRegion} from '../../models';
+
+/**
+ * The SubRegion node in the force graph
+ *
+ * Note 1: here the selector is given square brackets [] so that it can be
+ * inserted in SVG element like a directive
+ * Note 2: the selector is exactly the same as the @Input alias to make this
+ * directive trick work
+ */
+@Component({
+ selector: '[onos-subregionnodesvg]',
+ templateUrl: './subregionnodesvg.component.html',
+ styleUrls: ['./subregionnodesvg.component.css']
+})
+export class SubRegionNodeSvgComponent implements OnChanges {
+ @Input() subRegion: SubRegion;
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (!this.subRegion.x) {
+ this.subRegion.x = 0;
+ this.subRegion.y = 0;
+ }
+ }
+
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.css
new file mode 100644
index 0000000..056a0a0
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.css
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.gridrect {
+ stroke-width: 1;
+ fill: none;
+}
+
+.gridtext {
+ fill: lightgray;
+ text-anchor: middle;
+ dominant-baseline: middle;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.html
new file mode 100644
index 0000000..c183ac1
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.html
@@ -0,0 +1,48 @@
+<!--
+~ Copyright 2019-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<svg:g *ngFor="let pt of gridPointsHoriz" xmlns:svg="http://www.w3.org/2000/svg"
+ [attr.transform]="'translate(' + horizCentreOffset + ',' + vertCentreOffset + '), ' +
+ 'scale(' + gridScaleX + ',' + gridScaleY +')'">
+ <svg:desc>Vertical grid lines</svg:desc>
+ <svg:rect id="gridRectVert" class="gridrect"
+ [ngStyle]="{'stroke': gridcolor, 'stroke-width': 1/gridScaleX }"
+ [attr.width]="spacing"
+ [attr.height]="vertUpperLimit-vertLowerLimit"
+ [attr.x]="pt"
+ [attr.y]="vertLowerLimit">
+ </svg:rect>
+ <svg:text id="gridTextVert" class="gridtext"
+ [ngStyle]="{'stroke': gridcolor, 'font-size': 100/gridScaleX+'%', 'stroke-width': 1/gridScaleX }"
+ [attr.x]="pt"
+ [attr.y]="(vertUpperLimit - vertLowerLimit)/2">{{pt}}</svg:text>
+</svg:g>
+
+<svg:g *ngFor="let pt of gridPointsVert" xmlns:svg="http://www.w3.org/2000/svg"
+ [attr.transform]="'translate(' + horizCentreOffset + ',' + vertCentreOffset + '), ' +
+ 'scale(' + gridScaleX + ',' + gridScaleY + ')'">
+ <svg:desc>Horizontal grid lines</svg:desc>
+ <svg:rect id="gridRectHoriz" class="gridrect"
+ [ngStyle]="{'stroke': gridcolor, 'stroke-width': 1/gridScaleY }"
+ [attr.width]="horizUpperLimit-horizLowerLimit"
+ [attr.height]="spacing"
+ [attr.x]="horizLowerLimit"
+ [attr.y]="pt">
+ </svg:rect>
+ <svg:text id="gridTextHoriz" class="gridtext"
+ [ngStyle]="{'stroke': gridcolor, 'font-size': 100/gridScaleY+'%', 'stroke-width': 1/gridScaleY }"
+ [attr.x]="(horizUpperLimit - horizLowerLimit)/2"
+ [attr.y]="invertVertical ? -pt : pt">{{pt}}</svg:text>
+</svg:g>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.spec.ts
new file mode 100644
index 0000000..48a031a
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { GridsvgComponent } from './gridsvg.component';
+
+describe('GridsvgComponent', () => {
+ let component: GridsvgComponent;
+ let fixture: ComponentFixture<GridsvgComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ GridsvgComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(GridsvgComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.ts
new file mode 100644
index 0000000..e0934be
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/gridsvg/gridsvg.component.ts
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Component,
+ Input,
+ OnChanges,
+ OnInit,
+ SimpleChanges
+} from '@angular/core';
+
+/**
+ * How to fit in to the 1000 by 100 SVG viewbox
+ */
+export enum FitOption {
+ FIT1000WIDE = 'fit1000wide',
+ FIT1000HIGH = 'fit1000high',
+ FITNONE = 'fitnone'// 1:1 ratio
+}
+
+const SVG_VIEWBOX_CENTRE = 500; // View box is 0,0,1000,1000
+
+@Component({
+ selector: '[onos-gridsvg]',
+ templateUrl: './gridsvg.component.html',
+ styleUrls: ['./gridsvg.component.css']
+})
+export class GridsvgComponent implements OnInit, OnChanges {
+ @Input() horizLowerLimit: number = 0;
+ @Input() horizUpperLimit: number = 1000;
+ @Input() vertLowerLimit: number = 0;
+ @Input() vertUpperLimit: number = 1000;
+ @Input() spacing: number = 100;
+ @Input() invertVertical: boolean = false;
+ @Input() gridcolor: string = '#e8e7e1'; // If specifying this in a template use [gridcolor]="'#e8e7e1'"
+ @Input() centre: boolean = true;
+ @Input() fit: FitOption = FitOption.FITNONE;
+ @Input() aspectRatio: number = 1.0;
+
+ gridPointsHoriz: number[];
+ gridPointsVert: number[];
+ horizCentreOffset: number = 0;
+ vertCentreOffset: number = 0;
+ gridScaleX: number = 1.0;
+ gridScaleY: number = 1.0;
+
+ public static calculateGridPoints(lwr: number, upper: number, step: number): number[] {
+ const gridPoints = new Array<number>();
+ for (let i = lwr; i < upper; i += step) {
+ gridPoints.push(i);
+ }
+ return gridPoints;
+ }
+
+ public static calcOffset(lwr: number, upper: number): number {
+ return -((upper + lwr) * (upper - lwr) / ((upper - lwr) * 2) - SVG_VIEWBOX_CENTRE);
+ }
+
+ public static calcScale(lwr: number, upper: number): number {
+ return SVG_VIEWBOX_CENTRE * 2 / Math.abs(upper - lwr);
+ }
+
+ constructor() { }
+
+ ngOnInit() {
+ this.gridPointsHoriz = GridsvgComponent.calculateGridPoints(
+ this.horizLowerLimit, this.horizUpperLimit, this.spacing);
+ this.gridPointsVert = GridsvgComponent.calculateGridPoints(
+ this.vertLowerLimit, this.vertUpperLimit, this.spacing);
+ this.horizCentreOffset = GridsvgComponent.calcOffset(this.horizUpperLimit, this.horizLowerLimit);
+ this.vertCentreOffset = GridsvgComponent.calcOffset(this.vertUpperLimit, this.vertLowerLimit);
+ this.gridScaleX = this.whichScale(this.fit, true);
+ this.gridScaleY = this.whichScale(this.fit, false);
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['horizLowerLimit'] ||
+ changes['horizUpperLimit'] ||
+ changes['horizSpacing']) {
+ this.gridPointsHoriz = GridsvgComponent.calculateGridPoints(
+ this.horizLowerLimit, this.horizUpperLimit, this.spacing);
+ this.horizCentreOffset = GridsvgComponent.calcOffset(this.horizUpperLimit, this.horizLowerLimit);
+ }
+ if (changes['vertLowerLimit'] ||
+ changes['vertUpperLimit'] ||
+ changes['vertSpacing'] ) {
+ this.gridPointsVert = GridsvgComponent.calculateGridPoints(
+ this.vertLowerLimit, this.vertUpperLimit, this.spacing);
+ this.vertCentreOffset = GridsvgComponent.calcOffset(this.vertUpperLimit, this.vertLowerLimit);
+ }
+ }
+
+ whichScale(fit: FitOption, isX: boolean): number {
+ if (fit === FitOption.FIT1000HIGH) {
+ return GridsvgComponent.calcScale(
+ this.vertUpperLimit, this.vertLowerLimit) * (isX ? this.aspectRatio : 1.0);
+ } else if (fit === FitOption.FIT1000WIDE) {
+ return GridsvgComponent.calcScale(
+ this.horizUpperLimit, this.horizLowerLimit) * (isX ? 1.0 : this.aspectRatio);
+ } else {
+ return 1.0;
+ }
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.css
new file mode 100644
index 0000000..d35aa7b
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.css
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * ONOS GUI -- Topology Map Layer -- CSS file
+ */
+/* --- Topo Map --- */
+
+path.topo-map {
+ stroke-width: 0.05px;
+ stroke: #f4f4f4;
+ fill: #e5e5e6;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.html
new file mode 100644
index 0000000..cc2a794
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.html
@@ -0,0 +1,22 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<svg:desc xmlns:svg="http://www.w3.org/2000/svg">Map of {{map.id}} in SVG format</svg:desc>
+<svg:path class="topo-map"
+ *ngFor="let f of geodata?.features"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ [attr.d]="pathGenerator(f)">
+ <svg:title>{{ f.id }} {{f.properties?.name}}</svg:title>
+</svg:path>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.spec.ts
new file mode 100644
index 0000000..ab73820
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.spec.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MapSvgComponent } from './mapsvg.component';
+import {HttpClient} from '@angular/common/http';
+import {from} from 'rxjs';
+import {LogService} from 'gui2-fw-lib';
+
+class MockHttpClient {
+ get() {
+ return from(['{"id":"app","icon":"nav_apps","cat":"PLATFORM","label":"Applications"}']);
+ }
+
+ subscribe() {}
+}
+
+describe('MapSvgComponent', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: MapSvgComponent;
+ let fixture: ComponentFixture<MapSvgComponent>;
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+
+ TestBed.configureTestingModule({
+ declarations: [ MapSvgComponent ],
+ providers: [
+ { provide: LogService, useValue: logSpy },
+ { provide: HttpClient, useClass: MockHttpClient },
+ ]
+ })
+ .compileComponents();
+
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MapSvgComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.ts
new file mode 100644
index 0000000..ec9bdae
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/mapsvg/mapsvg.component.ts
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Component, EventEmitter,
+ Input,
+ OnChanges, Output,
+ SimpleChanges
+} from '@angular/core';
+import { MapObject } from '../maputils';
+import {LogService, MapBounds} from 'gui2-fw-lib';
+import {HttpClient} from '@angular/common/http';
+import * as d3 from 'd3';
+import * as topojson from 'topojson-client';
+
+const BUNDLED_URL_PREFIX = 'data/map/';
+
+/**
+ * Model of the transform attribute of a topojson file
+ */
+interface TopoDataTransform {
+ scale: number[];
+ translate: number[];
+}
+
+/**
+ * Model of the Feature returned prom topojson library
+ */
+interface Feature {
+ geometry: Object;
+ id: string;
+ properties: Object;
+ type: string;
+}
+
+/**
+ * Model of the Features Collection returned by the topojson.features function
+ */
+interface FeatureCollection {
+ type: string;
+ features: Feature[];
+}
+
+/**
+ * Model of the topojson file
+ */
+interface TopoData {
+ type: string; // Usually "Topology"
+ objects: Object; // Can be a list of countries or individual countries
+ arcs: number[][][]; // Coordinates
+ bbox: number[]; // Bounding box
+ transform: TopoDataTransform; // scale and translate
+}
+
+@Component({
+ selector: '[onos-mapsvg]',
+ templateUrl: './mapsvg.component.html',
+ styleUrls: ['./mapsvg.component.css']
+})
+export class MapSvgComponent implements OnChanges {
+ @Input() map: MapObject = <MapObject>{id: 'none'};
+ @Output() mapBounds = new EventEmitter<MapBounds>();
+
+ geodata: FeatureCollection;
+ pathgen: any; // (Feature) => string; have to leave it general, as there is the bounds method used below
+ // testPath: string;
+ // testFeature = <Feature>{
+ // id: 'test',
+ // type: 'Feature',
+ // geometry: {
+ // coordinates: [
+ // [[-15, 60], [45, 60], [45, 45], [-15, 45], [-15, 60]],
+ // [[-10, 55], [45, 55], [45, 50], [-10, 50], [-10, 55]],
+ // ],
+ // type: 'Polygon'
+ // },
+ // properties: { name: 'Test'}
+ // };
+
+ constructor(
+ private log: LogService,
+ private httpClient: HttpClient,
+ ) {
+ // Scale everything to 360 degrees wide and 150 high
+ // See background.component.html for more details
+ this.pathgen = d3.geoPath().projection(d3.geoIdentity().reflectY(true)
+ // MapSvgComponent.scale()
+ );
+
+ // this.log.debug('Feature Test',this.testFeature);
+ // this.testPath = this.pathgen(this.testFeature);
+ // this.log.debug('Feature Path', this.testPath);
+ this.log.debug('MapSvgComponent constructed');
+ }
+
+ static getUrl(id: string): string {
+ if (id && id[0] === '*') {
+ return BUNDLED_URL_PREFIX + id.slice(1) + '.topojson';
+ }
+ return id + '.topojson';
+ }
+
+ /**
+ * Wrapper for the path generator function
+ * @param feature The county or state within the map
+ */
+ pathGenerator(feature: Feature): string {
+ return this.pathgen(feature);
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ this.log.debug('Change detected', changes);
+ if (changes['map']) {
+ const map: MapObject = <MapObject>(changes['map'].currentValue);
+ if (map.id) {
+ this.httpClient
+ .get(MapSvgComponent.getUrl(map.filePath))
+ .subscribe((topoData: TopoData) => {
+ // this.mapPathGenerator =
+ this.handleTopoJson(map, topoData);
+ this.log.debug('Path Generated for', map.id,
+ 'from', MapSvgComponent.getUrl(map.filePath));
+ });
+ }
+ }
+ }
+
+ /**
+ * Handle the topojson file stream as it arrives back from the server
+ *
+ * The topojson library converts the topojson file in to a FeatureCollection
+ * d3.geo then further converts this in to a Path
+ *
+ * @param map The Map chosen in the GUI
+ * @param topoData The data in the TopoJson file
+ */
+ handleTopoJson(map: MapObject, topoData: TopoData): void {
+
+ let topoObject = topoData.objects[map.id];
+ if (!topoObject) {
+ topoObject = topoData.objects['states'];
+ }
+ this.log.debug('Topo obj', topoObject, 'topodata', topoData);
+ this.geodata = <FeatureCollection>topojson.feature(topoData, topoObject);
+ const bounds = this.pathgen.bounds(this.geodata);
+ this.mapBounds.emit(<MapBounds>{
+ lngMin: bounds[0][0],
+ latMin: -bounds[0][1], // Y was inverted in the transform
+ lngMax: bounds[1][0],
+ latMax: -bounds[1][1] // Y was inverted in the transform
+ });
+ this.log.debug('Map retrieved', topoData, this.geodata);
+
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/maputils.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/maputils.ts
new file mode 100644
index 0000000..45c3140
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/maputils.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export interface MapObject {
+ id: string;
+ description: string;
+ filePath: string;
+ scale: number;
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.css
new file mode 100644
index 0000000..7897595
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.css
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ ONOS GUI -- Topology View (no devices connected) -- CSS file
+ */
+/* --- "No Devices" Layer --- */
+#topo-noDevsLayer {
+ visibility: hidden;
+}
+
+#topo-noDevsLayer text {
+ font-size: 60pt;
+ font-style: italic;
+ fill: #7e9aa8;
+}
+
+#topo-noDevsLayer .noDevsBird {
+ fill: #db7773;
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.html
new file mode 100644
index 0000000..efe044f
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.html
@@ -0,0 +1,23 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<svg:g xmlns:svg="http://www.w3.org/2000/svg" id="topo-noDevsLayer"
+ [attr.transform]="'translate(' + (zoomExtents.tx) + ',' + (450 * zoomExtents.sc) + '),' +
+ 'scale(' + zoomExtents.sc + ')'"
+ style="visibility: visible;">
+ <svg:use width="100" height="100" class="noDevsBird" href="#bird"></svg:use>
+ <svg:text x="120" y="80">{{lionFn('no_devices_are_connected')}}</svg:text>
+</svg:g>
+
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.spec.ts
new file mode 100644
index 0000000..7f8af08
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.spec.ts
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { NoDeviceConnectedSvgComponent } from './nodeviceconnectedsvg.component';
+import { DebugElement } from '@angular/core';
+import { By } from '@angular/platform-browser';
+import {
+ FnService,
+ IconService,
+ LionService,
+ LogService,
+ UrlFnService,
+ TableFilterPipe,
+ IconComponent,
+ WebSocketService, SvgUtilService, PrefsService
+} from 'gui2-fw-lib';
+import { of } from 'rxjs';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+class MockWebSocketService {
+ createWebSocket() { }
+ isConnected() { return false; }
+ unbindHandlers() { }
+ bindHandlers() { }
+}
+
+class MockSvgUtilService {
+ translate() {}
+ scale() {}
+}
+
+class MockPrefsService {
+}
+
+
+/**
+ * ONOS GUI -- Topology NoDevicesConnected -- Unit Tests
+ */
+describe('NoDeviceConnectedSvgComponent', () => {
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: NoDeviceConnectedSvgComponent;
+ let fixture: ComponentFixture<NoDeviceConnectedSvgComponent>;
+
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({'debug': 'panel'});
+
+ windowMock = <any>{
+ innerWidth: 800,
+ innerHeight: 600,
+ };
+ fs = new FnService(ar, logSpy, windowMock);
+
+ TestBed.configureTestingModule({
+ declarations: [ NoDeviceConnectedSvgComponent ],
+ providers: [
+ { provide: FnService, useValue: fs },
+ { provide: LogService, useValue: logSpy },
+ { provide: SvgUtilService, useClass: MockSvgUtilService },
+ { provide: WebSocketService, useClass: MockWebSocketService },
+ { provide: PrefsService, useClass: MockPrefsService },
+ { provide: 'Window', useValue: windowMock },
+ ]
+ }).compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NoDeviceConnectedSvgComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.ts
new file mode 100644
index 0000000..d21d101
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ AfterContentInit,
+ Component,
+ HostListener,
+ Inject,
+ Input,
+ OnInit
+} from '@angular/core';
+import { ViewControllerImpl } from '../viewcontroller';
+import {
+ FnService,
+ LogService,
+ PrefsService,
+ SvgUtilService, LionService, ZoomUtils, TopoZoomPrefs
+} from 'gui2-fw-lib';
+
+/**
+ * ONOS GUI -- Topology No Connected Devices View.
+ * View that contains the 'No Connected Devices' message
+ *
+ * This component is an SVG snippet that expects to be in an SVG element with a view box of 1000x1000
+ *
+ * It should be added to a template with a tag like <svg:g onos-nodeviceconnected />
+ */
+@Component({
+ selector: '[onos-nodeviceconnected]',
+ templateUrl: './nodeviceconnectedsvg.component.html',
+ styleUrls: ['./nodeviceconnectedsvg.component.css']
+})
+export class NoDeviceConnectedSvgComponent extends ViewControllerImpl implements AfterContentInit, OnInit {
+ @Input() bannerHeight: number = 48;
+ lionFn; // Function
+ zoomExtents: TopoZoomPrefs = <TopoZoomPrefs>{
+ sc: 1.0, tx: 0, ty: 0
+ };
+
+ constructor(
+ protected fs: FnService,
+ protected log: LogService,
+ protected ps: PrefsService,
+ protected sus: SvgUtilService,
+ private lion: LionService,
+ @Inject('Window') public window: any,
+ ) {
+ super(fs, log, ps);
+
+ if (this.lion.ubercache.length === 0) {
+ this.lionFn = this.dummyLion;
+ this.lion.loadCbs.set('topo-nodevices', () => this.doLion());
+ } else {
+ this.doLion();
+ }
+
+ this.log.debug('NoDeviceConnectedSvgComponent constructed');
+ }
+
+ ngOnInit() {
+ this.log.debug('NoDeviceConnectedSvgComponent initialized');
+ }
+
+ ngAfterContentInit(): void {
+ this.zoomExtents = ZoomUtils.zoomToWindowSize(
+ this.bannerHeight, this.window.innerWidth, this.window.innerHeight);
+ }
+
+ @HostListener('window:resize', ['$event'])
+ onResize(event) {
+ this.zoomExtents = ZoomUtils.zoomToWindowSize(
+ this.bannerHeight, event.target.innerWidth, event.target.innerHeight);
+ }
+
+ /**
+ * Read the LION bundle for Details panel and set up the lionFn
+ */
+ doLion() {
+ this.lionFn = this.lion.bundle('core.view.Topo');
+
+ }
+
+ /**
+ * A dummy implementation of the lionFn until the response is received and the LION
+ * bundle is received from the WebSocket
+ */
+ dummyLion(key: string): string {
+ return '%' + key + '%';
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/viewcontroller.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/viewcontroller.ts
new file mode 100644
index 0000000..1b40195
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/viewcontroller.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { FnService, LogService, PrefsService } from 'gui2-fw-lib';
+
+export interface ViewControllerPrefs {
+ visible: string;
+}
+
+/*
+ ONOS GUI -- View Controller.
+ A base class for view controllers to extend from
+ */
+export abstract class ViewControllerImpl {
+ id: string;
+ displayName: string = 'View';
+ name: string;
+ prefs: ViewControllerPrefs;
+ visibility: string;
+
+ constructor(
+ protected fs: FnService,
+ protected log: LogService,
+ protected ps: PrefsService
+ ) {
+ this.log.debug('View Controller constructed');
+ }
+
+ initialize() {
+ this.name = this.displayName.toLowerCase().replace(/ /g, '_');
+ this.prefs = {
+ visible: this.name + '_visible',
+ };
+ }
+
+ enabled() {
+ return this.ps.getPrefs('topo2_prefs', null)[this.prefs.visible];
+ }
+
+ isVisible() {
+ return this.visibility;
+ }
+
+ hide() {
+ this.visibility = 'hidden';
+ }
+
+ show() {
+ this.visibility = 'visible';
+ }
+
+ toggle() {
+ if (this.visibility === 'hidden') {
+ this.visibility = 'visible';
+ } else if (this.visibility === 'visible') {
+ this.visibility = 'hidden';
+ }
+ }
+
+ lookupPrefState(key: string): number {
+ // Return 0 if not defined
+ return this.ps.getPrefs('topo2_prefs', null)[key] || 0;
+ }
+
+ updatePrefState(key: string, value: number) {
+ const state = this.ps.getPrefs('topo2_prefs', null);
+ state[key] = value ? 1 : 0;
+ this.ps.setPrefs('topo2_prefs', state);
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/zoomable.directive.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/zoomable.directive.spec.ts
new file mode 100644
index 0000000..23d75c5
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/zoomable.directive.spec.ts
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ZoomableDirective } from './zoomable.directive';
+import {inject, TestBed} from '@angular/core/testing';
+import {LogService, ConsoleLoggerService, FnService} from 'gui2-fw-lib';
+import {ElementRef} from '@angular/core';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+describe('ZoomableDirective', () => {
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let log: LogService;
+ let mockWindow: Window;
+
+ beforeEach(() => {
+ log = new ConsoleLoggerService();
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+ mockWindow = <any>{
+ navigator: {
+ userAgent: 'HeadlessChrome',
+ vendor: 'Google Inc.'
+ },
+ location: <any>{
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true' },
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+
+ fs = new FnService(ar, log, mockWindow);
+
+ TestBed.configureTestingModule({
+ providers: [ZoomableDirective,
+ { provide: FnService, useValue: fs },
+ { provide: LogService, useValue: log },
+ { provide: 'Window', useValue: mockWindow },
+ { provide: ElementRef, useValue: mockWindow }
+ ]
+ });
+ });
+
+ it('should create an instance', inject([ZoomableDirective], (directive: ZoomableDirective) => {
+
+ expect(directive).toBeTruthy();
+ }));
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/zoomable.directive.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/zoomable.directive.ts
new file mode 100644
index 0000000..9564444
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/zoomable.directive.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Directive,
+ ElementRef,
+ Input,
+ OnChanges,
+ OnInit,
+ SimpleChanges
+} from '@angular/core';
+import {LogService, PrefsService, TopoZoomPrefs} from 'gui2-fw-lib';
+import * as d3 from 'd3';
+
+const TOPO_ZOOM_PREFS = 'topo_zoom';
+
+const ZOOM_PREFS_DEFAULT: TopoZoomPrefs = <TopoZoomPrefs>{
+ tx: 0, ty: 0, sc: 1.0
+};
+
+/**
+ * A directive that takes care of Zooming and Panning the Topology view
+ *
+ * It wraps the D3 Pan and Zoom functionality
+ * See https://github.com/d3/d3-zoom/blob/master/README.md
+ */
+@Directive({
+ selector: '[onosZoomableOf]'
+})
+export class ZoomableDirective implements OnChanges, OnInit {
+ @Input() zoomableOf: ElementRef;
+
+ zoom: any; // The d3 zoom behaviour
+ zoomCached: TopoZoomPrefs = <TopoZoomPrefs>{tx: 0, ty: 0, sc: 1.0};
+
+ constructor(
+ private _element: ElementRef,
+ private log: LogService,
+ private ps: PrefsService
+ ) {
+ const container = d3.select(this._element.nativeElement);
+
+ const zoomed = () => {
+ const transform = d3.event.transform;
+ container.attr('transform', 'translate(' + transform.x + ',' + transform.y + ') scale(' + transform.k + ')');
+ this.updateZoomState(<TopoZoomPrefs>{tx: transform.x, ty: transform.y, sc: transform.k});
+ };
+
+ this.zoom = d3.zoom().on('zoom', zoomed);
+ }
+
+ ngOnInit() {
+ this.zoomCached = this.ps.getPrefs(TOPO_ZOOM_PREFS, ZOOM_PREFS_DEFAULT);
+ const svg = d3.select(this.zoomableOf);
+
+ svg.call(this.zoom);
+
+ svg.transition().call(this.zoom.transform,
+ d3.zoomIdentity.translate(this.zoomCached.tx, this.zoomCached.ty).scale(this.zoomCached.sc));
+ this.log.debug('Loaded topo_zoom_prefs',
+ this.zoomCached.tx, this.zoomCached.ty, this.zoomCached.sc);
+
+ }
+
+ /**
+ * Updates the cache of zoom preferences locally and onwards to the PrefsService
+ */
+ updateZoomState(zoomPrefs: TopoZoomPrefs): void {
+ this.zoomCached = zoomPrefs;
+ this.ps.setPrefs(TOPO_ZOOM_PREFS, zoomPrefs);
+ }
+
+ /**
+ * If the input object is changed then re-establish the zoom
+ */
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['zoomableOf']) {
+ const svg = d3.select(changes['zoomableOf'].currentValue);
+ svg.call(this.zoom);
+ this.log.debug('Applying zoomable behaviour on', this.zoomableOf, this._element.nativeElement);
+ }
+ }
+
+ /**
+ * Change the zoom level when a map is chosen in Topology view
+ *
+ * Animated to run over 750ms
+ */
+ changeZoomLevel(zoomState: TopoZoomPrefs, fast?: boolean): void {
+ const svg = d3.select(this.zoomableOf);
+ svg.transition().duration(fast ? 0 : 750).call(this.zoom.transform,
+ d3.zoomIdentity.translate(zoomState.tx, zoomState.ty).scale(zoomState.sc));
+ this.updateZoomState(zoomState);
+ this.log.debug('Pan to', zoomState.tx, zoomState.ty, 'and zoom to', zoomState.sc);
+ }
+
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.css
new file mode 100644
index 0000000..05402c8
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.css
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* --- Topo Details Panel --- */
+
+#topo2-p-detail {
+ padding: 16px;
+ opacity: 1;
+ right: 20px;
+ width: 260px;
+ top: 390px;
+}
+
+#topo2-p-detail div.actionBtns {
+ padding-top: 6px;
+}
+
+html[data-platform='iPad'] {
+ top: 386px;
+}
+
+.actionBtns .actionBtn {
+ display: inline-block;
+}
+.actionBtns .actionBtn svg {
+ width: 28px;
+ height: 28px;
+}
+
+div.actionBtns {
+ padding-top: 6px;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.html
new file mode 100644
index 0000000..ca23ca1
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.html
@@ -0,0 +1,66 @@
+<!--
+~ Copyright 2019-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<div id="topo2-p-detail" class="floatpanel topo2-p"
+ [@detailsPanelState]="on && selectedNode !== undefined">
+ <!-- Template explanation - Create a HTML header which has an SVG icon along
+ side title text. -->
+ <div class="header">
+ <div class="icon clickable">
+ <onos-icon
+ [iconSize]="26"
+ [iconId]="showDetails?.glyphId">
+ </onos-icon>
+ </div>
+ <h2 class="clickable">{{ showDetails?.title }}</h2>
+ </div>
+ <div class="body">
+ <table>
+ <tbody>
+ <!-- Template explanation - Inside a HTML table, create a row per
+ item in the propOrder array returned through the WSS showDetails.
+ If the row name contains only '-' then draw a horiz rule otherwise
+ create a cell for the name and another for the value -->
+ <tr *ngFor="let p of showDetails?.propOrder">
+ <td *ngIf="showDetails?.propLabels[p] !== '-'"
+ class="label">{{showDetails?.propLabels[p]}} :</td>
+ <td *ngIf="showDetails?.propLabels[p] !== '-'"
+ class="value">{{ showDetails?.propValues[p]}}</td>
+ <!-- If the label is '-' then insert a horiz line -->
+ <td *ngIf="showDetails?.propLabels[p] === '-'"
+ colspan="2"><hr></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="footer">
+ <hr>
+ <div class="actionBtns">
+ <!-- Template explanation - Inside the panel footer, create an SVG icon
+ per entry in the buttons array returned from the WSS showDetails
+ The icons used here are loaded in the ForceSvgComponent
+ -->
+ <div *ngFor="let btn of showDetails?.buttons" class="actionBtn">
+ <onos-icon id="topo2-p-detail-core-{{ btn }}"
+ (click)="navto(buttonAttribs(btn).path, showDetails.navPath, showDetails.id)"
+ [iconSize]="25"
+ [iconId]="buttonAttribs(btn).gid"
+ [toolTip]="lionFn(buttonAttribs(btn).tt)"
+ classes="button icon selected">
+ </onos-icon>
+ </div>
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.spec.ts
new file mode 100644
index 0000000..620d931
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.spec.ts
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { of } from 'rxjs';
+import { DetailsComponent } from './details.component';
+
+import {
+ FnService, LionService,
+ LogService, IconComponent
+} from 'gui2-fw-lib';
+import {RouterTestingModule} from '@angular/router/testing';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+/**
+ * ONOS GUI -- Topology View Details Panel-- Unit Tests
+ */
+describe('DetailsComponent', () => {
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: DetailsComponent;
+ let fixture: ComponentFixture<DetailsComponent>;
+
+ const bundleObj = {
+ 'core.view.Flow': {
+ test: 'test1'
+ }
+ };
+ const mockLion = (key) => {
+ return bundleObj[key] || '%' + key + '%';
+ };
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+ windowMock = <any>{
+ location: <any>{
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true' },
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+ fs = new FnService(ar, logSpy, windowMock);
+
+ TestBed.configureTestingModule({
+ imports: [ BrowserAnimationsModule, RouterTestingModule ],
+ declarations: [ DetailsComponent, IconComponent ],
+ providers: [
+ { provide: FnService, useValue: fs },
+ { provide: LogService, useValue: logSpy },
+ {
+ provide: LionService, useFactory: (() => {
+ return {
+ bundle: ((bundleId) => mockLion),
+ ubercache: new Array(),
+ loadCbs: new Map<string, () => void>([])
+ };
+ })
+ },
+ { provide: 'Window', useValue: windowMock },
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DetailsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.ts
new file mode 100644
index 0000000..ea1462c
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.component.ts
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Component,
+ Input,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ SimpleChanges
+} from '@angular/core';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+import {
+ DetailsPanelBaseImpl,
+ FnService, LionService,
+ LoadingService,
+ LogService,
+ WebSocketService
+} from 'gui2-fw-lib';
+import {Host, Link, LinkType, UiElement} from '../../layer/forcesvg/models';
+import {Params, Router} from '@angular/router';
+
+
+interface ButtonAttrs {
+ gid: string;
+ tt: string;
+ path: string;
+}
+
+const SHOWDEVICEVIEW: ButtonAttrs = {
+ gid: 'deviceTable',
+ tt: 'tt_ctl_show_device',
+ path: 'device',
+};
+const SHOWFLOWVIEW: ButtonAttrs = {
+ gid: 'flowTable',
+ tt: 'title_flows',
+ path: 'flow',
+};
+const SHOWPORTVIEW: ButtonAttrs = {
+ gid: 'portTable',
+ tt: 'tt_ctl_show_port',
+ path: 'port',
+};
+const SHOWGROUPVIEW: ButtonAttrs = {
+ gid: 'groupTable',
+ tt: 'tt_ctl_show_group',
+ path: 'group',
+};
+const SHOWMETERVIEW: ButtonAttrs = {
+ gid: 'meterTable',
+ tt: 'tt_ctl_show_meter',
+ path: 'meter',
+};
+const SHOWPIPECONFVIEW: ButtonAttrs = {
+ gid: 'pipeconfTable',
+ tt: 'tt_ctl_show_pipeconf',
+ path: 'pipeconf',
+};
+
+interface ShowDetails {
+ buttons: string[];
+ glyphId: string;
+ id: string;
+ navPath: string;
+ propLabels: Object;
+ propOrder: string[];
+ propValues: Object;
+ title: string;
+}
+/**
+ * ONOS GUI -- Topology Details Panel.
+ * Displays details of selected device. When no device is selected the panel slides
+ * off to the side and disappears
+ */
+@Component({
+ selector: 'onos-details',
+ templateUrl: './details.component.html',
+ styleUrls: [
+ './details.component.css', './details.theme.css',
+ '../../topology.common.css',
+ '../../../../fw/widget/panel.css', '../../../../fw/widget/panel-theme.css'
+ ],
+ animations: [
+ trigger('detailsPanelState', [
+ state('true', style({
+ transform: 'translateX(0%)',
+ opacity: '1.0'
+ })),
+ state('false', style({
+ transform: 'translateX(100%)',
+ opacity: '0'
+ })),
+ transition('0 => 1', animate('100ms ease-in')),
+ transition('1 => 0', animate('100ms ease-out'))
+ ])
+ ]
+})
+export class DetailsComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy, OnChanges {
+ @Input() selectedNode: UiElement = undefined; // Populated when user selects node or link
+ @Input() on: boolean = false; // Override the parent class attribute
+
+ // deferred localization strings
+ lionFn; // Function
+ showDetails: ShowDetails; // Will be populated on callback. Cleared if nothing is selected
+
+ constructor(
+ protected fs: FnService,
+ protected log: LogService,
+ protected ls: LoadingService,
+ protected router: Router,
+ protected wss: WebSocketService,
+ private lion: LionService
+ ) {
+ super(fs, ls, log, wss, 'topo');
+
+ if (this.lion.ubercache.length === 0) {
+ this.lionFn = this.dummyLion;
+ this.lion.loadCbs.set('flow', () => this.doLion());
+ } else {
+ this.doLion();
+ }
+
+ this.log.debug('Topo DetailsComponent constructed');
+ }
+
+ /**
+ * When the component is initializing set up the handler for callbacks of
+ * ShowDetails from the WSS. Set the variable showDetails when ever a callback
+ * is made
+ */
+ ngOnInit(): void {
+ this.wss.bindHandlers(new Map<string, (data) => void>([
+ ['showDetails', (data) => {
+ this.showDetails = data;
+ // this.log.debug('showDetails received', data);
+ }
+ ]
+ ]));
+ this.log.debug('Topo DetailsComponent initialized');
+ }
+
+ /**
+ * When the component is being unloaded then unbind the WSS handler.
+ */
+ ngOnDestroy(): void {
+ this.wss.unbindHandlers(['showDetails']);
+ this.log.debug('Topo DetailsComponent destroyed');
+ }
+
+ /**
+ * If changes are detected on the Input param selectedNode, call on WSS sendEvent
+ * Note the difference in call to the WSS with requestDetails between a node
+ * and a link - the handling is done in TopologyViewMessageHandler#RequestDetails.process()
+ *
+ * The WSS will call back asynchronously (see fn in ngOnInit())
+ *
+ * @param changes Simple Changes set of updates
+ */
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['selectedNode']) {
+ this.selectedNode = changes['selectedNode'].currentValue;
+ let type: any;
+
+ if (this.selectedNode === undefined) {
+ // Selection has been cleared
+ this.showDetails = <ShowDetails>{};
+ return;
+ }
+
+ if (this.selectedNode.hasOwnProperty('nodeType')) { // For Device, Host, SubRegion
+ type = (<Host>this.selectedNode).nodeType;
+ this.wss.sendEvent('requestDetails', {
+ id: this.selectedNode.id,
+ class: type,
+ });
+ } else if (this.selectedNode.hasOwnProperty('type')) { // Must be link
+ const link: Link = <Link>this.selectedNode;
+ if (<LinkType><unknown>LinkType[link.type] === LinkType.UiEdgeLink) { // Number based enum
+ this.wss.sendEvent('requestDetails', {
+ key: link.id,
+ class: 'link',
+ sourceId: link.epA,
+ targetId: Link.deviceNameFromEp(link.epB),
+ targetPort: link.portB,
+ isEdgeLink: true
+ });
+ } else {
+ this.wss.sendEvent('requestDetails', {
+ key: link.id,
+ class: 'link',
+ sourceId: Link.deviceNameFromEp(link.epA),
+ sourcePort: link.portA,
+ targetId: Link.deviceNameFromEp(link.epB),
+ targetPort: link.portB,
+ isEdgeLink: false
+ });
+ }
+ } else {
+ this.log.warn('Unexpected type for selected element', this.selectedNode);
+ }
+ }
+ }
+
+ /**
+ * Table of core button attributes to return per button icon
+ * @param btnName The name of the button
+ * @returns A structure with the button attributes
+ */
+ buttonAttribs(btnName: string): ButtonAttrs {
+ switch (btnName) {
+ case 'showDeviceView':
+ return SHOWDEVICEVIEW;
+ case 'showFlowView':
+ return SHOWFLOWVIEW;
+ case 'showPortView':
+ return SHOWPORTVIEW;
+ case 'showGroupView':
+ return SHOWGROUPVIEW;
+ case 'showMeterView':
+ return SHOWMETERVIEW;
+ case 'showPipeConfView':
+ return SHOWPIPECONFVIEW;
+ default:
+ return <ButtonAttrs>{
+ gid: btnName,
+ path: btnName
+ };
+ }
+ }
+
+ /**
+ * Navigate using Angular Routing. Combines the parameters to generate a relative URL
+ * e.g. if params are 'meter', 'device' and 'null:0000000000001' then the
+ * navigation URL will become "http://localhost:4200/#/meter?devId=null:0000000000000002"
+ *
+ * @param path The path to navigate to
+ * @param navPath The parameter name to use
+ * @param selId the parameter value to use
+ */
+ navto(path: string, navPath: string, selId: string): void {
+ this.log.debug('navigate to', path, 'for', navPath, '=', selId);
+ // Special case until it's fixed
+ if (selId) {
+ if (navPath === 'device') {
+ navPath = 'devId';
+ }
+ const queryPar: Params = {};
+ queryPar[navPath] = selId;
+ this.router.navigate([path], { queryParams: queryPar });
+ }
+ }
+
+ /**
+ * Read the LION bundle for Details panel and set up the lionFn
+ */
+ doLion() {
+ this.lionFn = this.lion.bundle('core.view.Flow');
+
+ }
+
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.theme.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.theme.css
new file mode 100644
index 0000000..7ad72dd
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/details/details.theme.css
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* --- Topo Details Panel Theme --- */
+
+#topo2-p-detail svg {
+ background: none;
+}
+
+#topo2-p-detail .header svg .glyph {
+ fill: #c0242b;
+}
+
+.dark #topo2-p-detail .header svg .glyph {
+ fill: #91292f;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.css
new file mode 100644
index 0000000..f335726
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.css
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* --- Topo Instance Panel --- */
+
+#topo-p-instance div.onosInst {
+ display: inline-block;
+ width: 170px;
+ height: 85px;
+ cursor: pointer;
+}
+
+#topo-p-instance svg text.instTitle {
+ font-size: 11pt;
+ font-weight: bold;
+ font-variant: small-caps;
+ text-transform: uppercase;
+}
+#topo-p-instance svg text.instLabel {
+ font-size: 10pt;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.html
new file mode 100644
index 0000000..f5a2859
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.html
@@ -0,0 +1,40 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<div id="topo-p-instance" class="floatpanel" [ngStyle]="{'left': '20px', 'top':divTopPx+'px', 'width': (onosInstances.length * 170)+'px', 'height': '85px'}" [@instancePanelState]="on">
+ <div *ngFor="let inst of onosInstances | keyvalue ; let i=index"
+ [ngClass]="['onosInst', inst.value.online?'online':'', inst.value.ready? 'ready': '', mastership?'mastership':'', 'affinity']"
+ (click)="chooseMastership(inst.value.id)">
+ <svg xmlns="http://www.w3.org/2000/svg" width="170" height="85" viewBox="0 0 170 85">
+ <!-- The following blue-glow effect is applied (through CSS) when mastership style is activated on a rectangle -->
+ <filter x="-50%" y="-50%" width="200%" height="200%" id="blue-glow">
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0 0 0 1 0 "></feColorMatrix>
+ <feGaussianBlur stdDeviation="3" result="coloredBlur"></feGaussianBlur>
+ <feMerge>
+ <feMergeNode in="coloredBlur"></feMergeNode>
+ <feMergeNode in="SourceGraphic"></feMergeNode>
+ </feMerge>
+ </filter>
+ <rect x="5" y="5" width="160" height="30" [ngStyle]="{ 'fill': panelColour(i)}"></rect>
+ <text class="instTitle" x="48" y="27">{{ inst.value.id }}</text>
+ <rect x="5" y="35" width="160" height="45"></rect>
+ <text class="instLabel ip" x="48" y="55">{{ inst.value.ip }}</text>
+ <use width="20" height="20" class="glyph badgeIcon bird" xlink:href="#bird" transform="translate(15,10)"></use>
+ <use *ngIf="inst.value.ready" width="16" height="16" class="glyph overlay badgeIcon readyBadge" xlink:href="#checkMark" transform="translate(18,40)"></use>
+ <text class="instLabel ns" x="48" y="73">{{lionFn('devices')}} {{ inst.value.switches }}</text>
+ <use *ngIf="inst.value.uiAttached" width="24" height="24" class="glyph overlay badgeIcon uiBadge" xlink:href="#uiAttached" transform="translate(14,54)"></use>
+ </svg>
+ </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.spec.ts
new file mode 100644
index 0000000..a03f23e
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.spec.ts
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { InstanceComponent } from './instance.component';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import {
+ FnService,
+ LogService
+} from 'gui2-fw-lib';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+/**
+ * ONOS GUI -- Topology View Instance Panel-- Unit Tests
+ */
+describe('InstanceComponent', () => {
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: InstanceComponent;
+ let fixture: ComponentFixture<InstanceComponent>;
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+ windowMock = <any>{
+ location: <any>{
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true' },
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+ fs = new FnService(ar, logSpy, windowMock);
+
+ TestBed.configureTestingModule({
+ imports: [ BrowserAnimationsModule ],
+ declarations: [ InstanceComponent ],
+ providers: [
+ { provide: FnService, useValue: fs },
+ { provide: LogService, useValue: logSpy },
+ { provide: 'Window', useValue: windowMock },
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(InstanceComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.ts
new file mode 100644
index 0000000..487550d
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.component.ts
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Component,
+ Input,
+ Output,
+ EventEmitter
+} from '@angular/core';
+import { animate, state, style, transition, trigger } from '@angular/animations';
+import {
+ LogService,
+ LoadingService,
+ FnService,
+ PanelBaseImpl,
+ IconService,
+ SvgUtilService, LionService
+} from 'gui2-fw-lib';
+
+/**
+ * A model of instance information that drives each panel
+ */
+export interface Instance {
+ id: string;
+ ip: string;
+ online: boolean;
+ ready: boolean;
+ switches: number;
+ uiAttached: boolean;
+}
+
+/**
+ * ONOS GUI -- Topology Instances Panel.
+ * Displays ONOS instances. The onosInstances Array gets updated by topology.service
+ * whenever a topo2AllInstances update arrives back on the WebSocket
+ *
+ * This emits a mastership event when the user clicks on an instance, to
+ * see the devices that it has mastership of.
+ */
+@Component({
+ selector: 'onos-instance',
+ templateUrl: './instance.component.html',
+ styleUrls: [
+ './instance.component.css', './instance.theme.css',
+ '../../topology.common.css',
+ '../../../../fw/widget/panel.css', '../../../../fw/widget/panel-theme.css'
+ ],
+ animations: [
+ trigger('instancePanelState', [
+ state('true', style({
+ transform: 'translateX(0%)',
+ opacity: '1.0'
+ })),
+ state('false', style({
+ transform: 'translateX(-100%)',
+ opacity: '0.0'
+ })),
+ transition('0 => 1', animate('100ms ease-in')),
+ transition('1 => 0', animate('100ms ease-out'))
+ ])
+ ]
+})
+export class InstanceComponent extends PanelBaseImpl {
+ @Input() divTopPx: number = 100;
+ @Input() on: boolean = false; // Override the parent class attribute
+ @Output() mastershipEvent = new EventEmitter<string>();
+ public onosInstances: Array<Instance>;
+ public mastership: string;
+ lionFn; // Function
+
+ constructor(
+ protected fs: FnService,
+ protected log: LogService,
+ protected ls: LoadingService,
+ protected is: IconService,
+ protected sus: SvgUtilService,
+ private lion: LionService
+ ) {
+ super(fs, ls, log);
+ this.onosInstances = <Array<Instance>>[];
+
+ if (this.lion.ubercache.length === 0) {
+ this.lionFn = this.dummyLion;
+ this.lion.loadCbs.set('topo-inst', () => this.doLion());
+ } else {
+ this.doLion();
+ }
+ this.log.debug('InstanceComponent constructed');
+ }
+
+ /**
+ * Get a colour for the banner of the nth panel
+ * @param idx The index of the panel (0-6)
+ */
+ panelColour(idx: number): string {
+ return this.sus.cat7().getColor(idx, false, '');
+ }
+
+ /**
+ * Toggle the display of mastership
+ * If the same instance is clicked a second time then cancel display of mastership
+ * @param instId The instance to display mastership for
+ */
+ chooseMastership(instId: string): void {
+ if (this.mastership === instId) {
+ this.mastership = '';
+ } else {
+ this.mastership = instId;
+ }
+ this.mastershipEvent.emit(this.mastership);
+ this.log.debug('Instance', this.mastership, 'chosen on GUI');
+ }
+
+ /**
+ * Read the LION bundle for Details panel and set up the lionFn
+ */
+ doLion() {
+ this.lionFn = this.lion.bundle('core.view.Topo');
+
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.theme.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.theme.css
new file mode 100644
index 0000000..3be7bdd
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/instance/instance.theme.css
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/* --- Topo Instance Panel --- */
+
+#topo-p-instance svg rect {
+ stroke-width: 0;
+ fill: #fbfbfb;
+}
+
+/* body of an instance */
+#topo-p-instance .online svg rect {
+ opacity: 1;
+ fill: #fbfbfb;
+}
+
+#topo-p-instance svg .glyph {
+ fill: #fff;
+}
+#topo-p-instance .online svg .glyph {
+ fill: #fff;
+}
+.dark #topo-p-instance .online svg .glyph.overlay {
+ fill: #fff;
+}
+
+/* offline */
+#topo-p-instance svg .badgeIcon {
+ opacity: 0.4;
+ fill: #939598;
+}
+
+/* online */
+#topo-p-instance .online svg .badgeIcon {
+ opacity: 1.0;
+ fill: #939598;
+}
+#topo-p-instance .online svg .badgeIcon.bird {
+ fill: #ffffff;
+}
+
+#topo-p-instance svg .readyBadge {
+ visibility: hidden;
+}
+#topo-p-instance .ready svg .readyBadge {
+ visibility: visible;
+}
+
+#topo-p-instance svg text {
+ text-anchor: start;
+ opacity: 0.5;
+ fill: #3c3a3a;
+}
+
+#topo-p-instance .online svg text {
+ opacity: 1.0;
+ fill: #3c3a3a;
+}
+
+#topo-p-instance .onosInst.mastership {
+ opacity: 0.3;
+}
+#topo-p-instance .onosInst.mastership.affinity {
+ opacity: 1.0;
+}
+#topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url(#blue-glow);
+}
+
+.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url(#blue-glow);
+}
+
+.dark #topo-p-instance {
+ background-color: #2f313c;
+ color: #c2c2b7;
+ border: 1px solid #364144;
+
+}
+
+.dark #topo-p-instance svg rect {
+ stroke-width: 0;
+ fill: #525660;
+}
+
+/* body of an instance */
+.dark #topo-p-instance .online svg rect {
+ opacity: 1;
+ fill: #838992;
+}
+
+.dark #topo-p-instance svg .glyph {
+ fill: #ddd;
+}
+.dark #topo-p-instance .online svg .glyph {
+ fill: #fff;
+}
+.dark #topo-p-instance .online svg .glyph.overlay {
+ fill: #c7c7c7;
+}
+
+/* offline */
+.dark #topo-p-instance svg .badgeIcon {
+ opacity: 0.4;
+ fill: #939598;
+}
+
+/* online */
+.dark #topo-p-instance .online svg .badgeIcon {
+ opacity: 1.0;
+ fill: #939598;
+}
+.dark #topo-p-instance .online svg .badgeIcon.bird {
+ fill: #ffffff;
+}
+
+.dark #topo-p-instance svg text {
+ text-anchor: start;
+ opacity: 0.5;
+ fill: #aaa;
+}
+
+.dark #topo-p-instance .online svg text {
+ opacity: 1.0;
+ fill: #fff;
+}
+
+.dark #topo-p-instance .onosInst.mastership {
+ opacity: 0.3;
+}
+.dark #topo-p-instance .onosInst.mastership.affinity {
+ opacity: 1.0;
+}
+.dark #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url(#blue-glow);
+}
+
+.dark.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+ filter: url(#blue-glow);
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.css
new file mode 100644
index 0000000..59c0d78
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.css
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * ONOS GUI -- Topology Map Selector -- CSS file
+ */
+.dialog h2 {
+ margin: 0;
+ word-wrap: break-word;
+ display: inline-block;
+ width: 210px;
+ vertical-align: middle;
+}
+
+.dialog .dialog-button {
+ display: inline-block;
+ cursor: pointer;
+ height: 20px;
+ padding: 6px 8px 2px 8px;
+ margin: 4px;
+ float: right;
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.html
new file mode 100644
index 0000000..579a43b
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.html
@@ -0,0 +1,33 @@
+<!--
+~ Copyright 2019-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<div id="topo-p-dialog"
+ class="floatpanel dialog topo-p"
+ style="opacity: 1; left: 20px; width: 300px;">
+ <div class="header">
+ <h2>{{ lionFn('title_select_map') }}</h2>
+ </div>
+ <div class="map-list">
+ <form [formGroup]="form">
+ <select formControlName="mapid">
+ <option *ngFor="let o of mapSelectorResponse.order" [ngValue]="o">{{ mapSelectorResponse.maps[o]['description'] }}</option>
+ </select>
+ </form>
+ </div>
+ <div class="footer">
+ <div class="dialog-button" (click)="choice(form.value)">{{ lionFn('ok') }}</div>
+ <div class="dialog-button" (click)="choice(undefined)">{{ lionFn('close') }}</div>
+ </div>
+</div>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.spec.ts
new file mode 100644
index 0000000..4d95361
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.spec.ts
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MapSelectorComponent } from './mapselector.component';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {FnService, LogService} from 'gui2-fw-lib';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+describe('MapSelectorComponent', () => {
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: MapSelectorComponent;
+ let fixture: ComponentFixture<MapSelectorComponent>;
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+ windowMock = <any>{
+ location: <any>{
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true' },
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+ fs = new FnService(ar, logSpy, windowMock);
+
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ ReactiveFormsModule
+ ],
+ declarations: [ MapSelectorComponent ],
+ providers: [
+ { provide: FnService, useValue: fs },
+ { provide: LogService, useValue: logSpy },
+ { provide: 'Window', useValue: windowMock },
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MapSelectorComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
+
+// Expecting WebSocket request and response similar to:
+//
+// {"event":"mapSelectorRequest","payload":{}}
+//
+// {
+// "event": "mapSelectorResponse",
+// "payload": {
+// "order": ["australia", "americas", "n_america", "s_america", "usa", "bayareaGEO",
+// "europe", "italy", "uk", "japan", "s_korea", "taiwan", "africa", "oceania", "asia"],
+// "maps": {
+// "australia": {
+// "id": "australia",
+// "description": "Australia",
+// "filePath": "*australia",
+// "scale": 1.0
+// },
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.ts
new file mode 100644
index 0000000..c97e0ca
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.component.ts
@@ -0,0 +1,87 @@
+
+import {
+ Component, EventEmitter, OnChanges,
+ OnDestroy,
+ OnInit, Output, SimpleChanges,
+} from '@angular/core';
+import {
+ DetailsPanelBaseImpl,
+ FnService,
+ LionService, LoadingService,
+ LogService,
+ WebSocketService
+} from 'gui2-fw-lib';
+import {FormControl, FormGroup} from '@angular/forms';
+import { MapObject } from '../../layer/maputils';
+
+interface MapSelection {
+ order: string[];
+ maps: Object[];
+}
+
+@Component({
+ selector: 'onos-mapselector',
+ templateUrl: './mapselector.component.html',
+ styleUrls: ['./mapselector.component.css', './mapselector.theme.css', '../../topology.common.css']
+})
+export class MapSelectorComponent extends DetailsPanelBaseImpl implements OnInit, OnDestroy {
+ @Output() chosenMap = new EventEmitter<MapObject>();
+ lionFn; // Function
+ mapSelectorResponse: MapSelection = <MapSelection>{
+ order: [],
+ maps: []
+ };
+ form = new FormGroup({
+ mapid: new FormControl(this.mapSelectorResponse.order[0]),
+ });
+
+ constructor(
+ protected fs: FnService,
+ protected log: LogService,
+ protected ls: LoadingService,
+ protected wss: WebSocketService,
+ private lion: LionService
+ ) {
+ super(fs, ls, log, wss, 'topo');
+
+ if (this.lion.ubercache.length === 0) {
+ this.lionFn = this.dummyLion;
+ this.lion.loadCbs.set('topoms', () => this.doLion());
+ } else {
+ this.doLion();
+ }
+
+ this.log.debug('Topo MapSelectorComponent constructed');
+ }
+
+ ngOnInit() {
+ this.wss.bindHandlers(new Map<string, (data) => void>([
+ ['mapSelectorResponse', (data) => {
+ this.mapSelectorResponse = data;
+ this.form.setValue({'mapid': this.mapSelectorResponse.order[0]});
+ }
+ ]
+ ]));
+ this.wss.sendEvent('mapSelectorRequest', {});
+ this.log.debug('Topo MapSelectorComponent initialized');
+ }
+
+ /**
+ * When the component is being unloaded then unbind the WSS handler.
+ */
+ ngOnDestroy(): void {
+ this.wss.unbindHandlers(['mapSelectorResponse']);
+ this.log.debug('Topo MapSelectorComponent destroyed');
+ }
+
+ /**
+ * Read the LION bundle for Details panel and set up the lionFn
+ */
+ doLion() {
+ this.lionFn = this.lion.bundle('core.view.Topo');
+ }
+
+ choice(mapid: Object): void {
+ this.chosenMap.emit(<MapObject>this.mapSelectorResponse.maps[mapid['mapid']]);
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.theme.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.theme.css
new file mode 100644
index 0000000..0ef1538
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/mapselector/mapselector.theme.css
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * ONOS GUI -- Topology Map Selector theme -- CSS file
+ */
+
+/*.light */
+.dialog .dialog-button {
+ background-color: #518ecc;
+ color: white;
+}
+
+
+/* ========== DARK Theme ========== */
+
+.dark .dialog .dialog-button {
+ background-color: #345e85;
+ color: #cccccd;
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.css
new file mode 100644
index 0000000..b4bd37b
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.css
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ ONOS GUI -- Topology Summary Panel -- CSS file
+ */
+#topo2-p-summary {
+ padding: 16px;
+ top: 100px;
+}
+
+#topo2-p-summary td.label {
+ width: 50%;
+}
+
+#topo2-p div.header div.icon {
+ padding: 10px
+}
+
+#topo2-p-summary div.header h2 {
+ padding: 10px;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.html
new file mode 100644
index 0000000..d781418
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.html
@@ -0,0 +1,20 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<div id="topo2-p-summary" class="floatpanel topo2-p"
+ style="opacity: 1; right: 20px; width: 260px;" [@summaryPanelState]="on">
+ <!-- everything else is filled in dynamically by listProps() and the
+ response showSummary received from the server -->
+</div>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.spec.ts
new file mode 100644
index 0000000..5f69c19
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.spec.ts
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { SummaryComponent } from './summary.component';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import {
+ FnService,
+ LogService
+} from 'gui2-fw-lib';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+/**
+ * ONOS GUI -- Topology View Summary Panel -- Unit Tests
+ */
+describe('SummaryComponent', () => {
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: SummaryComponent;
+ let fixture: ComponentFixture<SummaryComponent>;
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+ windowMock = <any>{
+ location: <any>{
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true' },
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+ fs = new FnService(ar, logSpy, windowMock);
+
+ TestBed.configureTestingModule({
+ imports: [ BrowserAnimationsModule ],
+ declarations: [ SummaryComponent ],
+ providers: [
+ { provide: FnService, useValue: fs },
+ { provide: LogService, useValue: logSpy },
+ { provide: 'Window', useValue: windowMock },
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SummaryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.ts
new file mode 100644
index 0000000..3a42a0b
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/summary/summary.component.ts
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Component,
+ Input,
+ OnDestroy,
+ OnInit,
+ ViewEncapsulation
+} from '@angular/core';
+import { animate, state, style, transition, trigger } from '@angular/animations';
+import * as d3 from 'd3';
+import { TopoPanelBaseImpl } from '../topopanel.base';
+import {
+ LogService,
+ LoadingService,
+ FnService,
+ WebSocketService,
+ GlyphService
+} from 'gui2-fw-lib';
+
+export interface SummaryResponse {
+ title: string;
+}
+/**
+ * ONOS GUI -- Topology Summary Module.
+ * Defines modeling of ONOS Summary Panel.
+ * Note: This component uses the d3 DOM building technique from the old GUI - this
+ * is not the Angular way of building components and should be avoided generally
+ * See DetailsPanelComponent for a better way of doing this kind of thing
+ */
+@Component({
+ selector: 'onos-summary',
+ templateUrl: './summary.component.html',
+ styleUrls: [
+ './summary.component.css',
+ '../../topology.common.css', '../../topology.theme.css',
+ '../../../../fw/widget/panel.css', '../../../../fw/widget/panel-theme.css'
+ ],
+ encapsulation: ViewEncapsulation.None,
+ animations: [
+ trigger('summaryPanelState', [
+ state('true', style({
+ transform: 'translateX(0%)',
+ opacity: '100'
+ })),
+ state('false', style({
+ transform: 'translateX(100%)',
+ opacity: '0'
+ })),
+ transition('0 => 1', animate('100ms ease-in')),
+ transition('1 => 0', animate('100ms ease-out'))
+ ])
+ ]
+})
+export class SummaryComponent extends TopoPanelBaseImpl implements OnInit, OnDestroy {
+ @Input() on: boolean = false; // Override the parent class attribute
+ private handlers: string[] = [];
+ private resp: string = 'showSummary';
+ private summaryData: SummaryResponse;
+
+ constructor(
+ protected fs: FnService,
+ protected log: LogService,
+ protected ls: LoadingService,
+ protected wss: WebSocketService,
+ protected gs: GlyphService
+ ) {
+ super(fs, ls, log, 'summary');
+ this.summaryData = <SummaryResponse>{};
+ this.log.debug('SummaryComponent constructed');
+ }
+
+
+ ngOnInit() {
+ this.wss.bindHandlers(new Map<string, (data) => void>([
+ [this.resp, (data) => this.handleSummaryData(data)]
+ ]));
+ this.handlers.push(this.resp);
+
+ this.init(d3.select('#topo2-p-summary'));
+
+ this.wss.sendEvent('requestSummary', {});
+ }
+
+ ngOnDestroy() {
+ this.wss.sendEvent('cancelSummary', {});
+ this.wss.unbindHandlers(this.handlers);
+ }
+
+ handleSummaryData(data: SummaryResponse) {
+ this.summaryData = data;
+ this.render();
+ }
+
+ private render() {
+ let endedWithSeparator;
+
+ this.emptyRegions();
+
+ const svg = this.appendToHeader('div')
+ .classed('icon', true)
+ .append('svg');
+ const title = this.appendToHeader('h2');
+ const table = this.appendToBody('table');
+ const tbody = table.append('tbody');
+
+ title.text(this.summaryData.title);
+ this.gs.addGlyph(svg, 'bird', 24, 0, [1, 1]);
+ endedWithSeparator = this.listProps(tbody, this.summaryData);
+ // TODO : review whether we need to use/store end-with-sep state
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/button.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/button.css
new file mode 100644
index 0000000..1effdbc
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/button.css
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Button Service (layout) -- CSS file
+ */
+
+.button,
+.toggleButton,
+.radioSet {
+ display: inline-block;
+ padding: 0 4px;
+}
+.radioButton {
+ display: inline-block;
+ padding: 0 2px;
+}
+
+.button svg.embeddedIcon,
+.toggleButton svg.embeddedIcon,
+.radioButton svg.embeddedIcon {
+ cursor: pointer;
+}
+.button svg.embeddedIcon .icon rect,
+.toggleButton svg.embeddedIcon .icon rect,
+.radioButton svg.embeddedIcon .icon rect{
+ stroke: none;
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.css
new file mode 100644
index 0000000..449d436
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.css
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ ONOS GUI -- Topology Toolbar Panel -- CSS file
+ */
+
+
+
+div.tbar-arrow {
+ position: absolute;
+ top: 53%;
+ left: 96%;
+ margin-right: -4%;
+ transform: translate(-50%, -50%);
+ cursor: pointer;
+}
+.safari div.tbar-arrow {
+ top: 46%;
+}
+.firefox div.tbar-arrow {
+ left: 97%;
+ margin-right: -3%;
+}
+
+.toolbar {
+ line-height: 125%;
+}
+.tbar-row {
+ display: inline-block;
+}
+
+.separator {
+ border: 1px solid;
+ margin: 0 4px 0 4px;
+ display: inline-block;
+ height: 23px;
+ width: 0;
+}
+
+#toolbar-topo2-toolbar {
+ padding: 6px;
+}
+
+#toolbar-topo2-toolbar .tbar-row.right {
+ width: 100%;
+}
+
+#toolbar-topo2-toolbar .tbar-row-text {
+ height: 21px;
+ text-align: right;
+ padding: 8px 60px 0 0;
+ font-style: italic;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.html
new file mode 100644
index 0000000..623c425
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.html
@@ -0,0 +1,77 @@
+<!--
+~ Copyright 2018-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<div id="toolbar-topo2-toolbar" class="floatpanel toolbar" [@toolbarState]="on"
+ style="opacity: 1; left: 0px; width: 286px; top: auto; bottom: 10px;">
+ <div class="tbar-arrow" (click)="on =! on">
+ <onos-icon [iconSize]="10" [iconId]="on?'triangleLeft':'triangleRight'"></onos-icon>
+ </div>
+ <div class="tbar-row ctrl-btns">
+ <div class="toggleButton" id="toolbar-topo2-toolbar-topo2-instance-tog" (click)="buttonClicked('instance-tog')">
+ <onos-icon [iconSize]="25" iconId="m_uiAttached" [toolTip]="lionFn('tbtt_tog_instances')" [classes]="['toggleButton', instancesVisible?'selected':'']"></onos-icon>
+ </div>
+ <div class="toggleButton" id="toolbar-topo2-toolbar-topo2-summary-tog" (click)="buttonClicked('summary-tog')">
+ <onos-icon [iconSize]="25" iconId="m_summary" [toolTip]="lionFn('tbtt_tog_summary')" [classes]="['toggleButton', summaryVisible?'selected':'']"></onos-icon>
+ </div>
+ <div class="toggleButton" id="toolbar-topo2-toolbar-details-tog" (click)="buttonClicked('details-tog')">
+ <onos-icon [iconSize]="25" iconId="m_details" [toolTip]="lionFn('tbtt_tog_use_detail')" [classes]="['toggleButton', detailsVisible?'selected':'']"></onos-icon>
+ </div>
+ <div class="separator"></div>
+ <div class="toggleButton" id="toolbar-topo2-toolbar-hosts-tog" (click)="buttonClicked('hosts-tog')">
+ <onos-icon [iconSize]="25" iconId="m_endstation" [toolTip]="lionFn('tbtt_tog_host')" [classes]="['toggleButton', hostsVisible?'selected':'']"></onos-icon>
+ </div>
+ <div class="toggleButton" id="toolbar-topo2-toolbar-offline-tog" (click)="buttonClicked('offline-tog')">
+ <onos-icon [iconSize]="25" iconId="m_switch" [toolTip]="lionFn('tbtt_tog_offline')" classes="toggleButton selected"></onos-icon>
+ </div>
+ <div class="toggleButton" id="toolbar-topo2-toolbar-topo2-ports-tog" (click)="buttonClicked('ports-tog')">
+ <onos-icon [iconSize]="25" iconId="m_ports" [toolTip]="lionFn('tbtt_tog_porthi')" classes="toggleButton selected" [classes]="['toggleButton', portsVisible?'selected':'']"></onos-icon>
+ </div>
+ <div class="toggleButton" id="toolbar-topo2-toolbar-topo2-bkgrnd-tog" (click)="buttonClicked('bkgrnd-tog')">
+ <onos-icon [iconSize]="25" iconId="m_map" [toolTip]="lionFn('tbtt_tog_map')" classes="toggleButton selected" [classes]="['toggleButton', backgroundVisible?'selected':'']"></onos-icon>
+ </div>
+ <div class="toggleButton" id="toolbar-topo2-toolbar-topo2-bkgrnd-sel" (click)="buttonClicked('bkgrnd-sel')">
+ <onos-icon [iconSize]="25" iconId="m_selectMap" [toolTip]="lionFn('tbtt_sel_map')" classes="button"></onos-icon>
+ </div>
+ </div>
+ <br>
+ <div class="tbar-row">
+ <div class="button" id="toolbar-topo2-toolbar-topo2-cycleLabels-btn" (click)="buttonClicked('cycleLabels-btn')">
+ <onos-icon [iconSize]="25" iconId="m_cycleLabels" [toolTip]="lionFn('tbtt_cyc_dev_labs')" classes="button"></onos-icon>
+ </div>
+ <div class="button" id="toolbar-topo2-toolbar-topo2-resetZoom-btn" (click)="buttonClicked('resetZoom-btn')">
+ <onos-icon [iconSize]="25" iconId="m_resetZoom" [toolTip]="lionFn('tbtt_reset_zoom')" classes="button"></onos-icon>
+ </div>
+ <div class="separator"></div>
+ <div class="button" id="toolbar-topo2-toolbar-topo2-eqMaster-btn" (click)="buttonClicked('eqMaster-btn')">
+ <onos-icon [iconSize]="25" iconId="m_eqMaster" [toolTip]="lionFn('tbtt_eq_master')" classes="button"></onos-icon>
+ </div>
+ <div class="separator"></div>
+ <div class="radioSet" id="toolbar-topo2-traffic">
+ <div class="radioButton selected" id="toolbar-topo2-cancel-traffic" (click)="buttonClicked('cancel-traffic')">
+ <onos-icon [iconSize]="25" iconId="m_unknown" [toolTip]="lionFn('tr_btn_cancel_monitoring')" classes="radioButton selected"></onos-icon>
+ </div>
+ <div class="radioButton" id="toolbar-topo2-all-traffic" (click)="buttonClicked('all-traffic')">
+ <onos-icon [iconSize]="25" iconId="m_allTraffic" [toolTip]="lionFn('tr_btn_show_related_traffic')" classes="radioButton selected"></onos-icon>
+ </div>
+ </div>
+ <div class="separator"></div>
+ <div class="button" id="toolbar-topo2-toolbar-topo2-quickhelp" (click)="buttonClicked('quickhelp-btn')">
+ <onos-icon [iconSize]="25" iconId="query" [toolTip]="lionFn('qh_title')" classes="button"></onos-icon>
+ </div>
+ <div class="button" id="toolbar-topo2-toolbar-topo2-cycleGrid-btn" (click)="buttonClicked('cycleGridDisplay-btn')">
+ <onos-icon [iconSize]="25" iconId="m_cycleGridDisplay" [toolTip]="lionFn('tbtt_cyc_grid_display')" classes="button"></onos-icon>
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.spec.ts
new file mode 100644
index 0000000..91c1de4
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.spec.ts
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { ToolbarComponent } from './toolbar.component';
+
+import {
+ FnService, LionService,
+ LogService, IconComponent
+} from 'gui2-fw-lib';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+/**
+ * ONOS GUI -- Topology View Topology Panel-- Unit Tests
+ */
+describe('ToolbarComponent', () => {
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: ToolbarComponent;
+ let fixture: ComponentFixture<ToolbarComponent>;
+
+ const bundleObj = {
+ 'core.view.Topo': {
+ test: 'test1'
+ }
+ };
+ const mockLion = (key) => {
+ return bundleObj[key] || '%' + key + '%';
+ };
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+ windowMock = <any>{
+ location: <any>{
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true' },
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+ fs = new FnService(ar, logSpy, windowMock);
+ TestBed.configureTestingModule({
+ imports: [ BrowserAnimationsModule ],
+ declarations: [ ToolbarComponent, IconComponent ],
+ providers: [
+ { provide: FnService, useValue: fs },
+ { provide: LogService, useValue: logSpy },
+ {
+ provide: LionService, useFactory: (() => {
+ return {
+ bundle: ((bundleId) => mockLion),
+ ubercache: new Array(),
+ loadCbs: new Map<string, () => void>([])
+ };
+ })
+ },
+ { provide: 'Window', useValue: windowMock },
+ ]
+ })
+ .compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ToolbarComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.ts
new file mode 100644
index 0000000..0615530
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.component.ts
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {
+ LogService,
+ LoadingService,
+ FnService,
+ PanelBaseImpl, LionService
+} from 'gui2-fw-lib';
+
+import {animate, state, style, transition, trigger} from '@angular/animations';
+
+export const INSTANCE_TOGGLE = 'instance-tog';
+export const SUMMARY_TOGGLE = 'summary-tog';
+export const DETAILS_TOGGLE = 'details-tog';
+export const HOSTS_TOGGLE = 'hosts-tog';
+export const OFFLINE_TOGGLE = 'offline-tog';
+export const PORTS_TOGGLE = 'ports-tog';
+export const BKGRND_TOGGLE = 'bkgrnd-tog';
+export const BKGRND_SELECT = 'bkgrnd-sel';
+export const CYCLELABELS_BTN = 'cycleLabels-btn';
+export const CYCLEHOSTLABEL_BTN = 'cycleHostLabel-btn';
+export const CYCLEGRIDDISPLAY_BTN = 'cycleGridDisplay-btn';
+export const RESETZOOM_BTN = 'resetZoom-btn';
+export const EQMASTER_BTN = 'eqMaster-btn';
+export const CANCEL_TRAFFIC = 'cancel-traffic';
+export const ALL_TRAFFIC = 'all-traffic';
+export const QUICKHELP_BTN = 'quickhelp-btn';
+
+
+/*
+ ONOS GUI -- Topology Toolbar Module.
+ Defines modeling of ONOS toolbar.
+ */
+@Component({
+ selector: 'onos-toolbar',
+ templateUrl: './toolbar.component.html',
+ styleUrls: [
+ './toolbar.component.css', './toolbar.theme.css',
+ '../../topology.common.css',
+ '../../../../fw/widget/panel.css', '../../../../fw/widget/panel-theme.css',
+ './button.css'
+ ],
+ animations: [
+ trigger('toolbarState', [
+ state('true', style({
+ transform: 'translateX(0%)',
+ // opacity: '1.0'
+ })),
+ state('false', style({
+ transform: 'translateX(-93%)',
+ // opacity: '0.0'
+ })),
+ transition('0 => 1', animate('500ms ease-in')),
+ transition('1 => 0', animate('500ms ease-out'))
+ ])
+ ]
+})
+export class ToolbarComponent extends PanelBaseImpl {
+ @Input() on: boolean = false; // Override the parent class attribute
+ // deferred localization strings
+ lionFn; // Function
+ // Used to drive the display of the hosts icon - there is also another such variable on the forcesvg
+ @Input() hostsVisible: boolean = false;
+ @Input() instancesVisible: boolean = true;
+ @Input() summaryVisible: boolean = true;
+ @Input() detailsVisible: boolean = true;
+ @Input() backgroundVisible: boolean = false;
+ @Input() portsVisible: boolean = true;
+
+ @Output() buttonEvent = new EventEmitter<string>();
+
+ constructor(
+ protected fs: FnService,
+ protected log: LogService,
+ protected ls: LoadingService,
+ private lion: LionService
+ ) {
+ super(fs, ls, log);
+
+ if (this.lion.ubercache.length === 0) {
+ this.lionFn = this.dummyLion;
+ this.lion.loadCbs.set('topo-toolbar', () => this.doLion());
+ } else {
+ this.doLion();
+ }
+
+ this.log.debug('ToolbarComponent constructed');
+ }
+
+ /**
+ * Read the LION bundle for Toolbar and set up the lionFn
+ */
+ doLion() {
+ this.lionFn = this.lion.bundle('core.view.Topo');
+ }
+
+ /**
+ * As buttons are clicked on the toolbar, emit events up to the parent
+ *
+ * The toggling of the input variables here is in addition to the control
+ * of these input variables from the parent. This is so that this component
+ * may be reused and is not dependent on a particular parent implementation
+ * to work
+ * @param name The name of button clicked.
+ */
+ buttonClicked(name: string): void {
+ switch (name) {
+ case HOSTS_TOGGLE:
+ this.hostsVisible = !this.hostsVisible;
+ break;
+ case INSTANCE_TOGGLE:
+ this.instancesVisible = !this.instancesVisible;
+ break;
+ case SUMMARY_TOGGLE:
+ this.summaryVisible = !this.summaryVisible;
+ break;
+ case DETAILS_TOGGLE:
+ this.detailsVisible = !this.detailsVisible;
+ break;
+ case BKGRND_TOGGLE:
+ this.backgroundVisible = !this.backgroundVisible;
+ break;
+ default:
+ }
+ // Send a message up to let TopologyComponent know of the event
+ this.buttonEvent.emit(name);
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.theme.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.theme.css
new file mode 100644
index 0000000..7933ee6
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/toolbar/toolbar.theme.css
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * ONOS GUI -- Topology Toolbar Panel -- Theme CSS file
+ */
+.tbar-arrow svg.embeddedIcon .icon rect {
+ stroke: none;
+}
+
+.tbar-arrow svg.embeddedIcon .icon .glyph {
+ fill: #838383;
+}
+
+.tbar-arrow svg.embeddedIcon .icon rect {
+ fill: none;
+}
+
+.separator {
+ border-color: #ddd;
+}
+
+/* ========== DARK Theme ========== */
+
+.dark .tbar-arrow svg.embeddedIcon .icon .glyph {
+ fill: #B2B2B2;
+}
+
+.dark .tbar-arrow svg.embeddedIcon .icon rect {
+ fill: none;
+}
+
+.dark .separator {
+ border-color: #454545;
+}
+
+.dark #toolbar-topo2-toolbar .tbar-row.right {
+ color: #666;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/topopanel.base.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/topopanel.base.ts
new file mode 100644
index 0000000..48aa2ed
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/panel/topopanel.base.ts
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ FnService,
+ LoadingService,
+ LogService,
+ PanelBaseImpl
+} from 'gui2-fw-lib';
+
+/**
+ * Base model of panel view - implemented by Topology Panel components
+ */
+export abstract class TopoPanelBaseImpl extends PanelBaseImpl {
+
+ protected header: any;
+ protected body: any;
+ protected footer: any;
+
+ protected constructor(
+ protected fs: FnService,
+ protected ls: LoadingService,
+ protected log: LogService,
+ protected id: string
+ ) {
+ super(fs, ls, log);
+ }
+
+ protected init(el: any) {
+ this.header = el.append('div').classed('header', true);
+ this.body = el.append('div').classed('body', true);
+ this.footer = el.append('div').classed('footer', true);
+ }
+
+ /**
+ * Decode lists of props sent back through Web Socket
+ *
+ * Means that panels do not have to know property names in advance
+ * Driven by PropertyPanel on Server side
+ */
+ listProps(el, data) {
+ let sepLast: boolean = false;
+
+ // note: track whether we end with a separator or not...
+ data.propOrder.forEach((p) => {
+ if (p === '-') {
+ this.addSep(el);
+ sepLast = true;
+ } else {
+ this.addProp(el, data.propLabels[p], data.propValues[p]);
+ sepLast = false;
+ }
+ });
+ return sepLast;
+ }
+
+ addProp(el, label, value) {
+ const tr = el.append('tr');
+ let lab;
+
+ if (typeof label === 'string') {
+ lab = label.replace(/_/g, ' ');
+ } else {
+ lab = label;
+ }
+
+ function addCell(cls, txt) {
+ tr.append('td').attr('class', cls).text(txt);
+ }
+
+ addCell('label', lab + ' :');
+ addCell('value', value);
+ }
+
+ addSep(el) {
+ el.append('tr').append('td').attr('colspan', 2).append('hr');
+ }
+
+ appendToHeader(x) {
+ return this.header.append(x);
+ }
+
+ appendToBody(x) {
+ return this.body.append(x);
+ }
+
+ appendToFooter(x) {
+ return this.footer.append(x);
+ }
+
+ emptyRegions() {
+ this.header.selectAll('*').remove();
+ this.body.selectAll('*').remove();
+ this.footer.selectAll('*').remove();
+ }
+
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology-routing.module.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology-routing.module.ts
new file mode 100644
index 0000000..66e17b6
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology-routing.module.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+import { TopologyComponent } from './topology/topology.component';
+
+const topologyRoutes: Routes = [
+ {
+ path: '',
+ component: TopologyComponent
+ },
+];
+
+/**
+ * ONOS GUI -- Topology Tabular View Feature Routing Module - allows it to be lazy loaded
+ *
+ * See https://angular.io/guide/lazy-loading-ngmodules
+ */
+@NgModule({
+ imports: [RouterModule.forChild(topologyRoutes)],
+ exports: [RouterModule]
+})
+export class TopologyRoutingModule { }
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.common.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.common.css
new file mode 100644
index 0000000..1ad9fbe
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.common.css
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * ONOS GUI -- Topology Common styles -- CSS file
+ */
+.topo2-p div.header {
+ margin-bottom: 10px;
+}
+
+.topo2-p div.header div.icon {
+ vertical-align: middle;
+ display: inline-block;
+}
+.topo2-p div.body {
+ overflow-y: scroll;
+}
+
+.topo2-p div.body::-webkit-scrollbar {
+ display: none;
+}
+
+.topo2-p svg {
+ display: inline-block;
+ width: 26px;
+ height: 26px;
+}
+
+
+.topo2-p h2 {
+ padding: 0 0 0 10px;
+ margin: 0;
+ font-weight: lighter;
+ word-wrap: break-word;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+.topo2-p h3 {
+ padding: 0 4px;
+ margin: 0;
+ word-wrap: break-word;
+ top: 20px;
+ left: 50px;
+}
+
+.topo2-p p,
+.topo2-p table {
+ padding: 0;
+ margin: 0;
+ width: 100%;
+}
+
+.topo2-p td {
+ word-wrap: break-word;
+}
+.topo2-p td.label {
+ font-weight: bold;
+ padding: 0 10px 0 0;
+}
+.topo2-p td.value {
+ padding: 0;
+}
+
+#topo2-p-summary td.label {
+ width: 50%;
+}
+
+.topo2-p hr {
+ height: 1px;
+ border: 0;
+ margin: 4px -3px;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.spec.ts
new file mode 100644
index 0000000..29d456f
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.spec.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { TestBed, inject } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import {of} from 'rxjs';
+
+import { TopologyService } from './topology.service';
+import {
+ LogService,
+ FnService
+} from 'gui2-fw-lib';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+/**
+ * ONOS GUI -- Topology Service - Unit Tests
+ */
+describe('TopologyService', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let ar: ActivatedRoute;
+ let fs: FnService;
+ let mockWindow: Window;
+
+ beforeEach(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn', 'info']);
+ ar = new MockActivatedRoute({'debug': 'TestService'});
+ mockWindow = <any>{
+ innerWidth: 400,
+ innerHeight: 200,
+ navigator: {
+ userAgent: 'defaultUA'
+ },
+ location: <any>{
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true' },
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+ fs = new FnService(ar, logSpy, mockWindow);
+
+ TestBed.configureTestingModule({
+ providers: [TopologyService,
+ { provide: FnService, useValue: fs},
+ { provide: LogService, useValue: logSpy },
+ { provide: ActivatedRoute, useValue: ar },
+ { provide: 'Window', useFactory: (() => mockWindow ) }
+ ]
+ });
+ logServiceSpy = TestBed.get(LogService);
+ });
+
+ it('should be created', inject([TopologyService], (service: TopologyService) => {
+ expect(service).toBeTruthy();
+ }));
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts
new file mode 100644
index 0000000..2c5d777
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.service.ts
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Injectable, SimpleChange} from '@angular/core';
+import {
+ LogService, WebSocketService,
+} from 'gui2-fw-lib';
+import { InstanceComponent } from './panel/instance/instance.component';
+import { BackgroundSvgComponent } from './layer/backgroundsvg/backgroundsvg.component';
+import { ForceSvgComponent } from './layer/forcesvg/forcesvg.component';
+import {
+ ModelEventMemo,
+ ModelEventType,
+ Region
+} from './layer/forcesvg/models';
+
+/**
+ * ONOS GUI -- Topology Service Module.
+ */
+@Injectable()
+export class TopologyService {
+
+ private handlers: string[] = [];
+ private openListener: any;
+
+ constructor(
+ protected log: LogService,
+ protected wss: WebSocketService
+ ) {
+ this.log.debug('TopologyService constructed');
+ }
+
+ /**
+ * bind our event handlers to the web socket service, so that our
+ * callbacks get invoked for incoming events
+ */
+ init(instance: InstanceComponent, background: BackgroundSvgComponent, force: ForceSvgComponent) {
+ this.wss.bindHandlers(new Map<string, (data) => void>([
+ ['topo2AllInstances', (data) => {
+ this.log.debug('Instances updated through WSS as topo2AllInstances', data);
+ instance.onosInstances = data.members;
+ }
+ ],
+ ['topo2CurrentLayout', (data) => {
+ this.log.debug('Background Data updated from WSS as topo2CurrentLayout', data);
+ if (background) {
+ background.layoutData = data;
+ }
+ }
+ ],
+ ['topo2CurrentRegion', (data) => {
+ force.regionData = data;
+ force.ngOnChanges({
+ 'regionData' : new SimpleChange(<Region>{}, data, true)
+ });
+ this.log.debug('Region Data replaced from WSS as topo2CurrentRegion', force.regionData);
+ }
+ ],
+ ['topo2PeerRegions', (data) => { this.log.warn('Add fn for topo2PeerRegions callback', data); } ],
+ ['topo2UiModelEvent', (event) => {
+ // this.log.debug('Handling', event);
+ force.handleModelEvent(
+ <ModelEventType><unknown>(ModelEventType[event.type]), // Number based enum
+ <ModelEventMemo>(event.memo), // String based enum
+ event.subject, event.data);
+ this.log.debug('Region Data updated from WSS as topo2UiModelEvent', force.regionData);
+ }
+ ],
+ // topo2Highlights is handled by TrafficService
+ ]));
+ this.handlers.push('topo2AllInstances');
+ this.handlers.push('topo2CurrentLayout');
+ this.handlers.push('topo2CurrentRegion');
+ this.handlers.push('topo2PeerRegions');
+ this.handlers.push('topo2UiModelEvent');
+ // this.handlers.push('topo2Highlights');
+
+ // in case we fail over to a new server,
+ // listen for wsock-open events
+ this.openListener = this.wss.addOpenListener(() => this.wsOpen);
+
+ // tell the server we are ready to receive topology events
+ this.wss.sendEvent('topo2Start', {});
+ this.log.debug('TopologyService initialized');
+ }
+
+ /**
+ * tell the server we no longer wish to receive topology events
+ */
+ destroy() {
+ this.wss.sendEvent('topo2Stop', {});
+ this.wss.unbindHandlers(this.handlers);
+ this.wss.removeOpenListener(this.openListener);
+ this.openListener = null;
+ this.log.debug('TopologyService destroyed');
+ }
+
+
+ wsOpen(host: string, url: string) {
+ this.log.debug('topo2Event: WSopen - cluster node:', host, 'URL:', url);
+ // tell the server we are ready to receive topo events
+ this.wss.sendEvent('topo2Start', {});
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.theme.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.theme.css
new file mode 100644
index 0000000..caa6199
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology.theme.css
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * ONOS GUI -- Topology Common styles -- CSS file
+ */
+
+.topo2-p h2 {
+ display: inline-block;
+ padding: 6px;
+}
+
+.topo2-p svg {
+ background: #c0242b;
+ width: 28px;
+ height: 28px;
+}
+
+.topo2-p svg .glyph {
+ fill: #ffffff;
+}
+
+.topo2-p hr {
+ background-color: #cccccc;
+}
+
+#topo2-p-detail svg {
+ background: none;
+}
+
+#topo2-p-detail .header svg .glyph {
+ fill: #c0242b;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.css b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.css
new file mode 100644
index 0000000..f1cde38
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.css
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/*
+ ONOS GUI -- Topology View (layout) -- CSS file
+ */
+/* --- Base SVG Layer --- */
+#ov-topo2 svg {
+ /* prevents the little cut/copy/paste square that would appear on iPad */
+ -webkit-user-select: none;
+ background-color: #f4f4f4;
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.html b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.html
new file mode 100644
index 0000000..1f6c07e
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.html
@@ -0,0 +1,92 @@
+<!--
+~ Copyright 2019-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<!-- Template explaination - Add in the flash message component - and link it to
+the local variable - this is used to display messages when keyboard shortcuts are pressed
+-->
+<onos-flash id="topoMsgFlash" message="{{ flashMsg }}" (closed)="flashMsg = ''"></onos-flash>
+
+<onos-quickhelp id="topoQuickHelp"></onos-quickhelp>
+<!-- Template explanation - Add in the Panel components for the Topology view
+ These are referenced inside the typescript by @ViewChild and their label
+-->
+<onos-instance #instance [divTopPx]="80"
+ (mastershipEvent)="force.onosInstMastership = $event"
+ [on]="prefsState.insts">
+</onos-instance>
+<onos-summary #summary [on]="prefsState.summary"></onos-summary>
+<onos-toolbar #toolbar
+ (buttonEvent)="toolbarButtonClicked($event)"
+ [on]="prefsState.toolbar"
+ [backgroundVisible]="prefsState.bg"
+ [detailsVisible]="prefsState.detail"
+ [hostsVisible]="prefsState.hosts"
+ [instancesVisible]="prefsState.insts"
+ [portsVisible]="prefsState.porthl"
+ [summaryVisible]="prefsState.summary">
+</onos-toolbar>
+<onos-details #details [on]="prefsState.detail"></onos-details>
+<onos-mapselector *ngIf="mapSelShown" (chosenMap)="changeMap($event)"></onos-mapselector>
+
+<div id="ov-topo2">
+ <!-- Template explanation -
+ Line 0) This is the root of the whole SVG canvas of the Topology View - all
+ components beneath it are SVG components only (no HTML)
+ line 1) the No Devices Connected banner is shown if the force component
+ (from line 4) does not contain any devices
+ line 2) Create an SVG Grouping and apply the onosZoomableOf directive to it,
+ passing in the whole SVG canvas (#svgZoom)
+ line 3) Add in the Background Svg Component (if showBackground is true - toggled
+ by toolbar and by keyboard shortcut 'B'
+ line 4) Add in the layer of the Force Svg Component. If any item is selected on it, pass
+ to the details view and deselect all others. This is node and line graph
+ whose contents are supplied through the Topology Service, and whose positions
+ are driven by the d3.force engine
+ -->
+ <svg:svg #svgZoom xmlns:svg="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" id="topo2"
+ preserveAspectRatio="xMaxYMax meet">
+ <svg:desc>The main SVG canvas of the Topology View</svg:desc>
+ <svg:g *ngIf="force.regionData?.devices[0].length +
+ force.regionData?.devices[1].length +
+ force.regionData?.devices[2].length=== 0"
+ onos-nodeviceconnected />
+ <svg:g id="topo-zoomlayer" onosZoomableOf [zoomableOf]="svgZoom">
+ <svg:desc>A logical layer that allows the main SVG canvas to be zoomed and panned</svg:desc>
+ <svg:g #gridFull *ngIf="prefsState.grid == 1 || prefsState.grid == 3" onos-gridsvg>
+ </svg:g>
+ <svg:g #geoGrid *ngIf="prefsState.grid == 2 || prefsState.grid == 3"
+ onos-gridsvg [horizLowerLimit]="-180" [horizUpperLimit]="180"
+ [vertLowerLimit]="-75" [vertUpperLimit]="75" [spacing]="15"
+ [invertVertical]="true" [fit]="'fit1000high'" [aspectRatio]="0.83333"
+ [gridcolor]="'#bfe7fb'">
+ </svg:g>
+ <svg:g *ngIf="prefsState.bg"
+ onos-backgroundsvg [map]="mapIdState" (zoomlevel)="mapExtentsZoom($event)">
+ <svg:desc>The Background SVG component - contains maps</svg:desc>
+ </svg:g>
+ <svg:g #force onos-forcesvg
+ [deviceLabelToggle]="prefsState.dlbls"
+ [hostLabelToggle]="prefsState.hlbls"
+ [showHosts]="prefsState.hosts"
+ [highlightPorts]="prefsState.porthl"
+ [scale]="window.innerHeight / (window.innerWidth * zoomDirective.zoomCached.sc)"
+ (selectedNodeEvent)="nodeSelected($event)">
+ <svg:desc>The Force SVG component - contains all the devices, hosts and links</svg:desc>
+ </svg:g>
+ </svg:g>
+ </svg:svg>
+</div>
+
+<div id="breadcrumbs"></div>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.spec.ts
new file mode 100644
index 0000000..b4d579d
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.spec.ts
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute, Params } from '@angular/router';
+import { of } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import * as d3 from 'd3';
+import { TopologyComponent } from './topology.component';
+import {
+ Instance,
+ InstanceComponent
+} from '../panel/instance/instance.component';
+import { SummaryComponent } from '../panel/summary/summary.component';
+import { ToolbarComponent } from '../panel/toolbar/toolbar.component';
+import { DetailsComponent } from '../panel/details/details.component';
+import { TopologyService } from '../topology.service';
+
+import {
+ FlashComponent,
+ QuickhelpComponent,
+ FnService,
+ LogService,
+ IconService, IconComponent, PrefsService, KeysService, LionService
+} from 'gui2-fw-lib';
+import {ZoomableDirective} from '../layer/zoomable.directive';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TrafficService} from '../traffic.service';
+import {ForceSvgComponent} from '../layer/forcesvg/forcesvg.component';
+import {DraggableDirective} from '../layer/forcesvg/draggable/draggable.directive';
+import {MapSelectorComponent} from '../panel/mapselector/mapselector.component';
+import {BackgroundSvgComponent} from '../layer/backgroundsvg/backgroundsvg.component';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {MapSvgComponent} from '../layer/mapsvg/mapsvg.component';
+import {GridsvgComponent} from '../layer/gridsvg/gridsvg.component';
+import {LinkSvgComponent} from '../layer/forcesvg/visuals/linksvg/linksvg.component';
+import {DeviceNodeSvgComponent} from '../layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component';
+import {SubRegionNodeSvgComponent} from '../layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component';
+import {HostNodeSvgComponent} from '../layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component';
+
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+class MockHttpClient {}
+
+class MockTopologyService {
+ init(instance: InstanceComponent) {
+ instance.onosInstances = [
+ <Instance>{
+ 'id': 'inst1',
+ 'ip': '127.0.0.1',
+ 'reachable': true,
+ 'online': true,
+ 'ready': true,
+ 'switches': 4,
+ 'uiAttached': true
+ },
+ <Instance>{
+ 'id': 'inst1',
+ 'ip': '127.0.0.2',
+ 'reachable': true,
+ 'online': true,
+ 'ready': true,
+ 'switches': 3,
+ 'uiAttached': false
+ }
+ ];
+ }
+ destroy() {}
+}
+
+class MockIconService {
+ loadIconDef() { }
+}
+
+class MockKeysService {
+ quickHelpShown: boolean = true;
+
+ keyBindings(x) {
+ return {};
+ }
+
+ gestureNotes() {
+ return {};
+ }
+}
+
+class MockTrafficService {}
+
+class MockPrefsService {
+ listeners: ((data) => void)[] = [];
+
+ getPrefs() {
+ return { 'topo2_prefs': ''};
+ }
+
+ addListener(listener: (data) => void): void {
+ this.listeners.push(listener);
+ }
+
+ removeListener(listener: (data) => void) {
+ this.listeners = this.listeners.filter((obj) => obj !== listener);
+ }
+
+ setPrefs(name: string, obj: Object) {
+
+ }
+
+}
+
+/**
+ * ONOS GUI -- Topology View -- Unit Tests
+ */
+describe('TopologyComponent', () => {
+ let fs: FnService;
+ let ar: MockActivatedRoute;
+ let windowMock: Window;
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let component: TopologyComponent;
+ let fixture: ComponentFixture<TopologyComponent>;
+
+ const bundleObj = {
+ 'core.fw.QuickHelp': {
+ test: 'test1',
+ tt_help: 'Help!'
+ }
+ };
+ const mockLion = (key) => {
+ return bundleObj[key] || '%' + key + '%';
+ };
+
+ beforeEach(async(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ ar = new MockActivatedRoute({ 'debug': 'txrx' });
+
+ windowMock = <any>{
+ location: <any>{
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true' },
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+ fs = new FnService(ar, logSpy, windowMock);
+
+ TestBed.configureTestingModule({
+ imports: [
+ BrowserAnimationsModule,
+ RouterTestingModule,
+ FormsModule,
+ ReactiveFormsModule
+ ],
+ declarations: [
+ TopologyComponent,
+ InstanceComponent,
+ SummaryComponent,
+ ToolbarComponent,
+ DetailsComponent,
+ FlashComponent,
+ ZoomableDirective,
+ IconComponent,
+ QuickhelpComponent,
+ ForceSvgComponent,
+ LinkSvgComponent,
+ DeviceNodeSvgComponent,
+ HostNodeSvgComponent,
+ DraggableDirective,
+ ZoomableDirective,
+ SubRegionNodeSvgComponent,
+ MapSelectorComponent,
+ BackgroundSvgComponent,
+ MapSvgComponent,
+ GridsvgComponent
+ ],
+ providers: [
+ { provide: FnService, useValue: fs },
+ { provide: LogService, useValue: logSpy },
+ { provide: 'Window', useValue: windowMock },
+ { provide: HttpClient, useClass: MockHttpClient },
+ { provide: TopologyService, useClass: MockTopologyService },
+ { provide: TrafficService, useClass: MockTrafficService },
+ { provide: IconService, useClass: MockIconService },
+ { provide: PrefsService, useClass: MockPrefsService },
+ { provide: KeysService, useClass: MockKeysService },
+ { provide: LionService, useFactory: (() => {
+ return {
+ bundle: ((bundleId) => mockLion),
+ ubercache: new Array(),
+ loadCbs: new Map<string, () => void>([])
+ };
+ })
+ },
+ ]
+ }).compileComponents();
+ logServiceSpy = TestBed.get(LogService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TopologyComponent);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.ts
new file mode 100644
index 0000000..d7055a4
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/topology/topology.component.ts
@@ -0,0 +1,674 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ AfterContentInit,
+ Component, HostListener, Inject, Input,
+ OnDestroy,
+ OnInit, SimpleChange,
+ ViewChild
+} from '@angular/core';
+import * as d3 from 'd3';
+import {
+ FnService, IconService,
+ KeysService,
+ KeysToken, LionService,
+ LogService,
+ PrefsService,
+ SvgUtilService,
+ WebSocketService,
+ TopoZoomPrefs, ZoomUtils
+} from 'gui2-fw-lib';
+import {InstanceComponent} from '../panel/instance/instance.component';
+import {DetailsComponent} from '../panel/details/details.component';
+import {BackgroundSvgComponent} from '../layer/backgroundsvg/backgroundsvg.component';
+import {ForceSvgComponent} from '../layer/forcesvg/forcesvg.component';
+import {TopologyService} from '../topology.service';
+import {
+ GridDisplayToggle,
+ HostLabelToggle,
+ LabelToggle,
+ UiElement
+} from '../layer/forcesvg/models';
+import {
+ INSTANCE_TOGGLE, SUMMARY_TOGGLE, DETAILS_TOGGLE,
+ HOSTS_TOGGLE, OFFLINE_TOGGLE, PORTS_TOGGLE,
+ BKGRND_TOGGLE, CYCLELABELS_BTN, CYCLEHOSTLABEL_BTN,
+ CYCLEGRIDDISPLAY_BTN, RESETZOOM_BTN, EQMASTER_BTN,
+ CANCEL_TRAFFIC, ALL_TRAFFIC, QUICKHELP_BTN, BKGRND_SELECT
+} from '../panel/toolbar/toolbar.component';
+import {TrafficService} from '../traffic.service';
+import {ZoomableDirective} from '../layer/zoomable.directive';
+import {MapObject} from '../layer/maputils';
+
+const TOPO2_PREFS = 'topo2_prefs';
+const TOPO_MAPID_PREFS = 'topo_mapid';
+
+const PREF_BG = 'bg';
+const PREF_DETAIL = 'detail';
+const PREF_DLBLS = 'dlbls';
+const PREF_HLBLS = 'hlbls';
+const PREF_GRID = 'grid';
+const PREF_HOSTS = 'hosts';
+const PREF_INSTS = 'insts';
+const PREF_OFFDEV = 'offdev';
+const PREF_PORTHL = 'porthl';
+const PREF_SUMMARY = 'summary';
+const PREF_TOOLBAR = 'toolbar';
+
+/**
+ * Model of the topo2_prefs object - this is a subset of the overall Prefs returned
+ * by the server
+ */
+export interface Topo2Prefs {
+ bg: number;
+ detail: number;
+ dlbls: number;
+ hlbls: number;
+ hosts: number;
+ insts: number;
+ offdev: number;
+ porthl: number;
+ spr: number;
+ ovid: string;
+ summary: number;
+ toolbar: number;
+ grid: number;
+}
+
+/**
+ * ONOS GUI Topology View
+ *
+ * This Topology View component is the top level component in a hierarchy that
+ * comprises the whole Topology View
+ *
+ * There are three main parts (panels, graphical and breadcrumbs)
+ * The panel hierarchy
+ * |-- Instances Panel (shows ONOS instances)
+ * |-- Summary Panel (summary of ONOS)
+ * |-- Toolbar Panel (the toolbar)
+ * |-- Details Panel (when a node is selected in the Force graphical view (see below))
+ *
+ * The graphical hierarchy contains
+ * Topology (this)
+ * |-- No Devices Connected (only of there are no nodes to show)
+ * |-- Zoom Layer (everything beneath this can be zoomed and panned)
+ * |-- Background (container for any backgrounds - can be toggled on and off)
+ * |-- Map
+ * |-- Forces (all of the nodes and links laid out by a d3.force simulation)
+ *
+ * The breadcrumbs
+ * |-- Breadcrumb (in region view a way of navigating back up through regions)
+ */
+@Component({
+ selector: 'onos-topology',
+ templateUrl: './topology.component.html',
+ styleUrls: ['./topology.component.css']
+})
+export class TopologyComponent implements AfterContentInit, OnInit, OnDestroy {
+ @Input() bannerHeight: number = 48;
+ // These are references to the components inserted in the template
+ @ViewChild(InstanceComponent) instance: InstanceComponent;
+ @ViewChild(DetailsComponent) details: DetailsComponent;
+ @ViewChild(BackgroundSvgComponent) background: BackgroundSvgComponent;
+ @ViewChild(ForceSvgComponent) force: ForceSvgComponent;
+ @ViewChild(ZoomableDirective) zoomDirective: ZoomableDirective;
+
+ flashMsg: string = '';
+ // These are used as defaults if nothing is set on the server
+ prefsState: Topo2Prefs = <Topo2Prefs>{
+ bg: 0,
+ detail: 1,
+ dlbls: 0,
+ hlbls: 2,
+ hosts: 0,
+ insts: 1,
+ offdev: 1,
+ ovid: 'traffic', // default to traffic overlay
+ porthl: 1,
+ spr: 0,
+ summary: 1,
+ toolbar: 0,
+ grid: 0
+ };
+
+ mapIdState: MapObject = <MapObject>{
+ id: undefined,
+ scale: 1.0
+ };
+ mapSelShown: boolean = false;
+ lionFn; // Function
+
+ gridShown: boolean = true;
+ geoGridShown: boolean = true;
+
+ constructor(
+ protected log: LogService,
+ protected fs: FnService,
+ protected ks: KeysService,
+ protected sus: SvgUtilService,
+ protected ps: PrefsService,
+ protected wss: WebSocketService,
+ protected ts: TopologyService,
+ protected trs: TrafficService,
+ protected is: IconService,
+ private lion: LionService,
+ @Inject('Window') public window: any,
+ ) {
+ if (this.lion.ubercache.length === 0) {
+ this.lionFn = this.dummyLion;
+ this.lion.loadCbs.set('topo-toolbar', () => this.doLion());
+ } else {
+ this.doLion();
+ }
+
+ this.is.loadIconDef('bird');
+ this.is.loadIconDef('active');
+ this.is.loadIconDef('uiAttached');
+ this.is.loadIconDef('m_switch');
+ this.is.loadIconDef('m_roadm');
+ this.is.loadIconDef('m_router');
+ this.is.loadIconDef('m_uiAttached');
+ this.is.loadIconDef('m_endstation');
+ this.is.loadIconDef('m_ports');
+ this.is.loadIconDef('m_summary');
+ this.is.loadIconDef('m_details');
+ this.is.loadIconDef('m_map');
+ this.is.loadIconDef('m_selectMap');
+ this.is.loadIconDef('m_cycleLabels');
+ this.is.loadIconDef('m_cycleGridDisplay');
+ this.is.loadIconDef('m_resetZoom');
+ this.is.loadIconDef('m_eqMaster');
+ this.is.loadIconDef('m_unknown');
+ this.is.loadIconDef('m_allTraffic');
+ this.is.loadIconDef('deviceTable');
+ this.is.loadIconDef('flowTable');
+ this.is.loadIconDef('portTable');
+ this.is.loadIconDef('groupTable');
+ this.is.loadIconDef('meterTable');
+ this.is.loadIconDef('triangleUp');
+ this.log.debug('Topology component constructed');
+ }
+
+ /**
+ * Static functions must come before member variables
+ * @param index Corresponds to LabelToggle.Enum index
+ */
+ private static deviceLabelFlashMessage(index: number): string {
+ switch (index) {
+ case 0: return 'fl_device_labels_hide';
+ case 1: return 'fl_device_labels_show_friendly';
+ case 2: return 'fl_device_labels_show_id';
+ }
+ }
+
+ private static hostLabelFlashMessage(index: number): string {
+ switch (index) {
+ case 0: return 'fl_host_labels_hide';
+ case 1: return 'fl_host_labels_show_friendly';
+ case 2: return 'fl_host_labels_show_ip';
+ case 3: return 'fl_host_labels_show_mac';
+ }
+ }
+
+ private static gridDisplayFlashMessage(index: number): string {
+ switch (index) {
+ case 0: return 'fl_grid_display_hide';
+ case 1: return 'fl_grid_display_1000';
+ case 2: return 'fl_grid_display_geo';
+ case 3: return 'fl_grid_display_both';
+ }
+ }
+
+ /**
+ * Pass the list of Key Commands to the KeyService, and initialize the Topology
+ * Service - which communicates with through the WebSocket to the ONOS server
+ * to get the nodes and links.
+ */
+ ngOnInit() {
+ this.bindCommands();
+ // The components from the template are handed over to TopologyService here
+ // so that WebSocket responses can be passed back in to them
+ // The handling of the WebSocket call is delegated out to the Topology
+ // Service just to compartmentalize things a bit
+ this.ts.init(this.instance, this.background, this.force);
+
+ this.ps.addListener((data) => this.prefsUpdateHandler(data));
+ this.prefsState = this.ps.getPrefs(TOPO2_PREFS, this.prefsState);
+ this.mapIdState = this.ps.getPrefs(TOPO_MAPID_PREFS, this.mapIdState);
+
+ this.log.debug('Topology component initialized');
+ }
+
+ ngAfterContentInit(): void {
+ // Scale the window initially - then after resize
+ const zoomMapExtents = ZoomUtils.zoomToWindowSize(
+ this.bannerHeight, this.window.innerWidth, this.window.innerHeight);
+ this.zoomDirective.changeZoomLevel(zoomMapExtents, true);
+ this.log.debug('Topology zoom initialized',
+ this.bannerHeight, this.window.innerWidth, this.window.innerHeight,
+ zoomMapExtents);
+ }
+
+ /**
+ * Callback function that's called whenever new Prefs are received from WebSocket
+ *
+ * Note: At present the backend server does not filter updated by logged in user,
+ * so you might get updates pertaining to a different user
+ */
+ prefsUpdateHandler(data: any): void {
+ // Extract the TOPO2 prefs from it
+ if (data[TOPO2_PREFS]) {
+ this.prefsState = data[TOPO2_PREFS];
+ }
+ this.log.debug('Updated topo2 prefs', this.prefsState, this.mapIdState);
+ }
+
+ /**
+ * When this component is being stopped, disconnect the TopologyService from
+ * the WebSocket
+ */
+ ngOnDestroy() {
+ this.ts.destroy();
+ this.ps.removeListener((data) => this.prefsUpdateHandler(data));
+ this.log.debug('Topology component destroyed');
+ }
+
+ @HostListener('window:resize', ['$event'])
+ onResize(event) {
+ const zoomMapExtents = ZoomUtils.zoomToWindowSize(
+ this.bannerHeight, event.target.innerWidth, event.target.innerHeight);
+ this.zoomDirective.changeZoomLevel(zoomMapExtents, true);
+ this.log.debug('Topology window resize',
+ event.target.innerWidth, event.target.innerHeight, this.bannerHeight, zoomMapExtents);
+ }
+
+ /**
+ * When ever a toolbar button is clicked, an event is sent up from toolbar
+ * component which is caught and passed on to here.
+ * @param name The name of the button that was clicked
+ */
+ toolbarButtonClicked(name: string) {
+ switch (name) {
+ case INSTANCE_TOGGLE:
+ this.toggleInstancePanel();
+ break;
+ case SUMMARY_TOGGLE:
+ this.toggleSummary();
+ break;
+ case DETAILS_TOGGLE:
+ this.toggleDetails();
+ break;
+ case HOSTS_TOGGLE:
+ this.toggleHosts();
+ break;
+ case OFFLINE_TOGGLE:
+ this.toggleOfflineDevices();
+ break;
+ case PORTS_TOGGLE:
+ this.togglePorts();
+ break;
+ case BKGRND_TOGGLE:
+ this.toggleBackground();
+ break;
+ case BKGRND_SELECT:
+ this.mapSelShown = !this.mapSelShown;
+ break;
+ case CYCLELABELS_BTN:
+ this.cycleDeviceLabels();
+ break;
+ case CYCLEHOSTLABEL_BTN:
+ this.cycleHostLabels();
+ break;
+ case CYCLEGRIDDISPLAY_BTN:
+ this.cycleGridDisplay();
+ break;
+ case RESETZOOM_BTN:
+ this.resetZoom();
+ break;
+ case EQMASTER_BTN:
+ this.equalizeMasters();
+ break;
+ case CANCEL_TRAFFIC:
+ this.cancelTraffic();
+ break;
+ case ALL_TRAFFIC:
+ this.monitorAllTraffic();
+ break;
+ case QUICKHELP_BTN:
+ this.ks.quickHelpShown = true;
+ break;
+ default:
+ this.log.warn('Unhandled Toolbar action', name);
+ }
+ }
+
+ /**
+ * The list of key strokes that will be active in the Topology View.
+ *
+ * This action map is passed to the KeyService through the bindCommands()
+ * when this component is being initialized
+ */
+ actionMap() {
+ return {
+ A: [() => {this.monitorAllTraffic(); }, 'Monitor all traffic'],
+ B: [(token) => {this.toggleBackground(token); }, 'Toggle background'],
+ D: [(token) => {this.toggleDetails(token); }, 'Toggle details panel'],
+ E: [() => {this.equalizeMasters(); }, 'Equalize mastership roles'],
+ H: [() => {this.toggleHosts(); }, 'Toggle host visibility'],
+ I: [(token) => {this.toggleInstancePanel(token); }, 'Toggle ONOS Instance Panel'],
+ G: [() => {this.mapSelShown = !this.mapSelShown; }, 'Show map selection dialog'],
+ L: [() => {this.cycleDeviceLabels(); }, 'Cycle device labels'],
+ M: [() => {this.toggleOfflineDevices(); }, 'Toggle offline visibility'],
+ O: [() => {this.toggleSummary(); }, 'Toggle the Summary Panel'],
+ P: [(token) => {this.togglePorts(token); }, 'Toggle Port Highlighting'],
+ Q: [() => {this.cycleGridDisplay(); }, 'Cycle grid display'],
+ R: [() => {this.resetZoom(); }, 'Reset pan / zoom'],
+ U: [() => {this.unpinNode(); }, 'Unpin node (mouse over)'],
+ X: [() => {this.resetNodeLocation(); }, 'Reset Node Location'],
+ dot: [() => {this.toggleToolbar(); }, 'Toggle Toolbar'],
+ 0: [() => {this.cancelTraffic(); }, 'Cancel traffic monitoring'],
+ 'shift-L': [() => {this.cycleHostLabels(); }, 'Cycle host labels'],
+
+ // -- instance color palette debug
+ 9: () => {
+ this.sus.cat7().testCard(d3.select('svg#topo2'));
+ },
+
+ esc: [() => {this.handleEscape(); }, 'Cancel commands'],
+
+ // TODO update after adding in Background Service
+ // topology overlay selections
+ // F1: function () { t2tbs.fnKey(0); },
+ // F2: function () { t2tbs.fnKey(1); },
+ // F3: function () { t2tbs.fnKey(2); },
+ // F4: function () { t2tbs.fnKey(3); },
+ // F5: function () { t2tbs.fnKey(4); },
+ //
+ // _keyListener: t2tbs.keyListener.bind(t2tbs),
+
+ _helpFormat: [
+ ['I', 'O', 'D', 'H', 'M', 'P', 'dash', 'B'],
+ ['X', 'Z', 'N', 'L', 'shift-L', 'U', 'R', 'E', 'dot'],
+ [], // this column reserved for overlay actions
+ ],
+ };
+ }
+
+
+ bindCommands(additional?: any) {
+
+ const am = this.actionMap();
+ const add = this.fs.isO(additional);
+
+ this.ks.keyBindings(am);
+
+ this.ks.gestureNotes([
+ ['click', 'Select the item and show details'],
+ ['shift-click', 'Toggle selection state'],
+ ['drag', 'Reposition (and pin) device / host'],
+ ['cmd-scroll', 'Zoom in / out'],
+ ['cmd-drag', 'Pan'],
+ ]);
+ }
+
+ handleEscape() {
+
+ if (false) {
+ // TODO: Cancel show mastership
+ // TODO: Cancel Active overlay
+ // TODO: Reinstate with components
+ } else {
+ this.nodeSelected(undefined);
+ this.log.debug('Handling escape');
+ // } else if (t2rs.deselectAllNodes()) {
+ // // else if we have node selections, deselect them all
+ // // (work already done)
+ // } else if (t2rs.deselectLink()) {
+ // // else if we have a link selection, deselect it
+ // // (work already done)
+ // } else if (t2is.isVisible()) {
+ // // If the instance panel is visible, close it
+ // t2is.toggle();
+ // } else if (t2sp.isVisible()) {
+ // // If the summary panel is visible, close it
+ // t2sp.toggle();
+ }
+ }
+
+ /**
+ * Updates the cache of preferences locally and onwards to the PrefsService
+ * @param what The attribute of the local topo2-prefs cache to update
+ * @param b the value to update it with
+ */
+ updatePrefsState(what: string, b: number) {
+ this.prefsState[what] = b;
+ this.ps.setPrefs(TOPO2_PREFS, this.prefsState);
+ }
+
+ /**
+ * When the button is clicked on the toolbar or the L key is pressed
+ * 1) cycle through options
+ * 2) flash up a message
+ * 3a) Update the local prefs cache
+ * 3b) And passes on to the global prefs service which sends back to the server
+ * 3c) It also has a knock on effect of passing it on to ForceSvgComponent
+ * because prefsState.dlbls is given as an input to it
+ * 3d) This will in turn pass it down to the DeviceSvgComponent which
+ * displays the label
+ */
+ protected cycleDeviceLabels() {
+ const old: LabelToggle.Enum = this.prefsState.dlbls;
+ const next = LabelToggle.next(old);
+ this.flashMsg = this.lionFn(TopologyComponent.deviceLabelFlashMessage(next));
+ this.updatePrefsState(PREF_DLBLS, next);
+ this.log.debug('Cycling device labels', old, next);
+ }
+
+ protected cycleHostLabels() {
+ const old: HostLabelToggle.Enum = this.prefsState.hlbls;
+ const next = HostLabelToggle.next(old);
+ this.flashMsg = this.lionFn(TopologyComponent.hostLabelFlashMessage(next));
+ this.updatePrefsState(PREF_HLBLS, next);
+ this.log.debug('Cycling host labels', old, next);
+ }
+
+ protected cycleGridDisplay() {
+ const old: GridDisplayToggle.Enum = this.prefsState.grid;
+ const next = GridDisplayToggle.next(old);
+ this.flashMsg = this.lionFn(TopologyComponent.gridDisplayFlashMessage(next));
+ this.updatePrefsState(PREF_GRID, next);
+ this.log.debug('Cycling grid display', old, next);
+ }
+
+ /**
+ * When the button is clicked on the toolbar or the B key is pressed
+ * 1) Find the inverse of the current state (held as 1 or 0)
+ * 2) Flash up a message on screen
+ * 3b) And passes on to the global prefs service which sends back to the server
+ * 3c) It also has a knock on effect of passing it on to ToolbarComponent
+ * because prefsState.bg is given as an input to it
+ * @param token not currently used
+ */
+ protected toggleBackground(token?: KeysToken) {
+ const bg: boolean = !Boolean(this.prefsState.bg);
+ this.flashMsg = this.lionFn(bg ? 'show' : 'hide') +
+ ' ' + this.lionFn('fl_background_map');
+ this.updatePrefsState(PREF_BG, bg ? 1 : 0);
+ this.log.debug('Toggling background', token, bg ? 'shown' : 'hidden');
+ }
+
+ protected toggleDetails(token?: KeysToken) {
+ const on: boolean = !Boolean(this.prefsState.detail);
+ this.flashMsg = this.lionFn(on ? 'show' : 'hide') +
+ ' ' + this.lionFn('fl_panel_details');
+ this.updatePrefsState(PREF_DETAIL, on ? 1 : 0);
+ this.log.debug('Toggling details', token);
+ }
+
+ protected toggleInstancePanel(token?: KeysToken) {
+ const on: boolean = !Boolean(this.prefsState.insts);
+ this.flashMsg = this.lionFn(on ? 'show' : 'hide') +
+ ' ' + this.lionFn('fl_panel_instances');
+ this.updatePrefsState(PREF_INSTS, on ? 1 : 0);
+ this.log.debug('Toggling instances', token, on);
+ }
+
+ protected toggleSummary() {
+ const on: boolean = !Boolean(this.prefsState.summary);
+ this.flashMsg = this.lionFn(on ? 'show' : 'hide') +
+ ' ' + this.lionFn('fl_panel_summary');
+ this.updatePrefsState(PREF_SUMMARY, on ? 1 : 0);
+ }
+
+ protected togglePorts(token?: KeysToken) {
+ const current: boolean = !Boolean(this.prefsState.porthl);
+ this.flashMsg = this.lionFn(current ? 'enable' : 'disable') +
+ ' ' + this.lionFn('fl_port_highlighting');
+ this.updatePrefsState(PREF_PORTHL, current ? 1 : 0);
+ this.log.debug(current ? 'Enable' : 'Disable', 'port highlighting');
+ }
+
+ protected toggleToolbar() {
+ const on: boolean = !Boolean(this.prefsState.toolbar);
+ this.updatePrefsState(PREF_TOOLBAR, on ? 1 : 0);
+ this.log.debug('toggling toolbar', on ? 'shown' : 'hidden');
+ }
+
+ protected toggleHosts() {
+ const current: boolean = !Boolean(this.prefsState.hosts);
+ this.flashMsg = this.lionFn('hosts') + ' ' +
+ this.lionFn(this.force.showHosts ? 'visible' : 'hidden');
+ this.updatePrefsState(PREF_HOSTS, current ? 1 : 0);
+ this.log.debug('toggling hosts: ', this.prefsState.hosts ? 'Show' : 'Hide');
+ }
+
+ protected toggleOfflineDevices() {
+ const on: boolean = !Boolean(this.prefsState.offdev);
+ this.flashMsg = this.lionFn(on ? 'show' : 'hide') +
+ ' ' + this.lionFn('fl_offline_devices');
+ this.updatePrefsState(PREF_OFFDEV, on ? 1 : 0);
+ this.log.debug('toggling offline devices', this.prefsState.offdev);
+ }
+
+ protected resetZoom() {
+ const zoomMapExtents = ZoomUtils.zoomToWindowSize(
+ this.bannerHeight, this.window.innerWidth, this.window.innerHeight);
+ this.zoomDirective.changeZoomLevel(zoomMapExtents, false);
+ this.flashMsg = this.lionFn('fl_pan_zoom_reset');
+ }
+
+ protected equalizeMasters() {
+ this.wss.sendEvent('equalizeMasters', null);
+ this.flashMsg = this.lionFn('fl_eq_masters');
+ this.log.debug('equalizing masters');
+ }
+
+ protected resetNodeLocation() {
+ // TODO: Implement reset locations
+ this.force.resetNodeLocations();
+ this.flashMsg = this.lionFn('fl_reset_node_locations');
+ this.log.debug('resetting node location');
+ }
+
+ protected unpinNode() {
+ // TODO: Implement this
+ this.log.debug('unpinning node');
+ }
+
+ /**
+ * Check to see if this is needed anymore
+ * @param what - a key stroke
+ */
+ protected notValid(what) {
+ this.log.warn('topo.js getActionEntry(): Not a valid ' + what);
+ }
+
+ /**
+ * Check to see if this is needed anymore
+ * @param key - a key stroke
+ */
+ getActionEntry(key) {
+ let entry;
+
+ if (!key) {
+ this.notValid('key');
+ return null;
+ }
+
+ entry = this.actionMap()[key];
+
+ if (!entry) {
+ this.notValid('actionMap (' + key + ') entry');
+ return null;
+ }
+ return this.fs.isA(entry) || [entry, ''];
+ }
+
+ /**
+ * An event handler that updates the details panel as items are
+ * selected in the forcesvg layer
+ * @param nodeOrLink the item to display details of
+ */
+ nodeSelected(nodeOrLink: UiElement) {
+ this.details.ngOnChanges({'selectedNode':
+ new SimpleChange(undefined, nodeOrLink, true)});
+ }
+
+ /**
+ * Enable traffic monitoring
+ */
+ monitorAllTraffic() {
+ // TODO: Implement support for toggling between bits, packets and octets
+ this.flashMsg = this.lionFn('tr_fl_pstats_bits');
+ this.trs.init(this.force);
+ }
+
+ /**
+ * Cancel traffic monitoring
+ */
+ cancelTraffic() {
+ this.flashMsg = this.lionFn('fl_monitoring_canceled');
+ this.trs.destroy();
+ }
+
+ changeMap(map: MapObject) {
+ this.mapSelShown = false; // Hide the MapSelector component
+ this.mapIdState = map;
+ this.ps.setPrefs(TOPO_MAPID_PREFS, this.mapIdState);
+ this.log.debug('Map has been changed to ', map);
+ }
+
+ mapExtentsZoom(zoomMapExtents: TopoZoomPrefs) {
+ // this.zoomDirective.updateZoomState(zoomPrefs.tx, zoomPrefs.ty, zoomPrefs.sc);
+ this.zoomDirective.changeZoomLevel(zoomMapExtents);
+ this.log.debug('Map zoom prefs updated', zoomMapExtents);
+ }
+
+ /**
+ * Read the LION bundle for Toolbar and set up the lionFn
+ */
+ doLion() {
+ this.lionFn = this.lion.bundle('core.view.Topo');
+ }
+
+ /**
+ * A dummy implementation of the lionFn until the response is received and the LION
+ * bundle is received from the WebSocket
+ */
+ dummyLion(key: string): string {
+ return '%' + key + '%';
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.spec.ts
new file mode 100644
index 0000000..8b2a736
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.spec.ts
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { TestBed } from '@angular/core/testing';
+
+import { TrafficService } from './traffic.service';
+import {FnService, LogService} from 'gui2-fw-lib';
+import {ActivatedRoute, Params} from '@angular/router';
+import {of} from 'rxjs';
+import {TopologyService} from './topology.service';
+
+class MockActivatedRoute extends ActivatedRoute {
+ constructor(params: Params) {
+ super();
+ this.queryParams = of(params);
+ }
+}
+
+describe('TrafficService', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let ar: ActivatedRoute;
+ let fs: FnService;
+ let mockWindow: Window;
+
+ beforeEach(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['debug', 'warn', 'info']);
+ ar = new MockActivatedRoute({'debug': 'TestService'});
+ mockWindow = <any>{
+ innerWidth: 400,
+ innerHeight: 200,
+ navigator: {
+ userAgent: 'defaultUA'
+ },
+ location: <any>{
+ hostname: 'foo',
+ host: 'foo',
+ port: '80',
+ protocol: 'http',
+ search: { debug: 'true' },
+ href: 'ws://foo:123/onos/ui/websock/path',
+ absUrl: 'ws://foo:123/onos/ui/websock/path'
+ }
+ };
+ fs = new FnService(ar, logSpy, mockWindow);
+
+ TestBed.configureTestingModule({
+ providers: [TopologyService,
+ { provide: FnService, useValue: fs},
+ { provide: LogService, useValue: logSpy },
+ { provide: ActivatedRoute, useValue: ar },
+ { provide: 'Window', useFactory: (() => mockWindow ) }
+ ]
+ });
+ logServiceSpy = TestBed.get(LogService);
+ });
+
+ it('should be created', () => {
+ const service: TrafficService = TestBed.get(TrafficService);
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.ts
new file mode 100644
index 0000000..b6d2b85
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/traffic.service.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Injectable } from '@angular/core';
+import {LogService, WebSocketService} from 'gui2-fw-lib';
+import {ForceSvgComponent} from './layer/forcesvg/forcesvg.component';
+
+export enum TrafficType {
+ IDLE,
+ FLOWSTATSBYTES = 'flowStatsBytes',
+ PORTSTATSBITSEC = 'portStatsBitSec',
+ PORTSTATSPKTSEC = 'portStatsPktSec',
+}
+
+const ALL_TRAFFIC_TYPES = [
+ TrafficType.FLOWSTATSBYTES,
+ TrafficType.PORTSTATSBITSEC,
+ TrafficType.PORTSTATSPKTSEC
+];
+
+const ALL_TRAFFIC_MSGS = [
+ 'Flow Stats (bytes)',
+ 'Port Stats (bits / second)',
+ 'Port Stats (packets / second)',
+];
+
+/**
+ * ONOS GUI -- Traffic Service Module.
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class TrafficService {
+ private handlers: string[] = [];
+ private openListener: any;
+
+ constructor(
+ protected log: LogService,
+ protected wss: WebSocketService
+ ) {
+ this.log.debug('TrafficService constructed');
+ }
+
+ init(force: ForceSvgComponent) {
+ this.wss.bindHandlers(new Map<string, (data) => void>([
+ ['topo2Highlights', (data) => {
+ force.handleHighlights(data.devices, data.hosts, data.links);
+ }
+ ]
+ ]));
+
+ this.handlers.push('topo2Highlights');
+
+ // in case we fail over to a new server,
+ // listen for wsock-open events
+ this.openListener = this.wss.addOpenListener(() => this.wsOpen);
+
+ // tell the server we are ready to receive topology events
+ this.wss.sendEvent('topo2RequestAllTraffic', {
+ trafficType: TrafficType.FLOWSTATSBYTES
+ });
+ this.log.debug('Topo2Traffic: Show All Traffic');
+ }
+
+ destroy() {
+ this.wss.sendEvent('topo2CancelTraffic', {});
+ this.wss.unbindHandlers(this.handlers);
+ this.log.debug('Traffic monitoring canceled');
+ }
+
+ wsOpen(host: string, url: string) {
+ this.log.debug('topo2RequestAllTraffic: WSopen - cluster node:', host, 'URL:', url);
+ // tell the server we are ready to receive topo events
+ this.wss.sendEvent('topo2RequestAllTraffic', {
+ trafficType: TrafficType.FLOWSTATSBYTES
+ });
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/public_api.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/public_api.ts
new file mode 100644
index 0000000..24d46a2
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/public_api.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Public API Surface of gui2-topo-lib
+ */
+
+export * from './lib/topology/topology.component';
+export * from './lib/topology.service';
+export * from './lib/traffic.service';
+export * from './lib/topology-routing.module';
+
+export * from './lib/layer/zoomable.directive';
+export * from './lib/layer/viewcontroller';
+export * from './lib/layer/maputils';
+
+export * from './lib/layer/backgroundsvg/backgroundsvg.component';
+export * from './lib/layer/gridsvg/gridsvg.component';
+export * from './lib/layer/mapsvg/mapsvg.component';
+export * from './lib/layer/nodeviceconnectedsvg/nodeviceconnectedsvg.component';
+
+export * from './lib/layer/forcesvg/forcesvg.component';
+export * from './lib/layer/forcesvg/draggable/draggable.directive';
+
+export * from './lib/layer/forcesvg/models/node';
+export * from './lib/layer/forcesvg/models/link';
+export * from './lib/layer/forcesvg/models/regions';
+export * from './lib/layer/forcesvg/models/force-directed-graph';
+
+export * from './lib/layer/forcesvg/visuals/devicenodesvg/devicenodesvg.component';
+export * from './lib/layer/forcesvg/visuals/hostnodesvg/hostnodesvg.component';
+export * from './lib/layer/forcesvg/visuals/linksvg/linksvg.component';
+export * from './lib/layer/forcesvg/visuals/subregionnodesvg/subregionnodesvg.component';
+export * from './lib/layer/forcesvg/visuals/nodevisual';
+
+export * from './lib/panel/details/details.component';
+export * from './lib/panel/instance/instance.component';
+export * from './lib/panel/mapselector/mapselector.component';
+export * from './lib/panel/summary/summary.component';
+export * from './lib/panel/toolbar/toolbar.component';
+export * from './lib/panel/topopanel.base';
+
+export * from './lib/gui2-topo-lib.module';
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/test.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/test.ts
new file mode 100644
index 0000000..f626203
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/test.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'core-js/es7/reflect';
+import 'zone.js/dist/zone';
+import 'zone.js/dist/zone-testing';
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: any;
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting()
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/tsconfig.lib.json b/web/gui2-topo-lib/projects/gui2-topo-lib/tsconfig.lib.json
new file mode 100644
index 0000000..3fe337f
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/tsconfig.lib.json
@@ -0,0 +1,32 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/lib",
+ "target": "es2015",
+ "module": "es2015",
+ "moduleResolution": "node",
+ "declaration": true,
+ "sourceMap": true,
+ "inlineSources": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "importHelpers": true,
+ "types": [],
+ "lib": [
+ "dom",
+ "es2018"
+ ]
+ },
+ "angularCompilerOptions": {
+ "annotateForClosureCompiler": true,
+ "skipTemplateCodegen": true,
+ "strictMetadataEmit": true,
+ "fullTemplateTypeCheck": true,
+ "strictInjectionParameters": true,
+ "enableResourceInlining": true
+ },
+ "exclude": [
+ "src/test.ts",
+ "**/*.spec.ts"
+ ]
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/tsconfig.spec.json b/web/gui2-topo-lib/projects/gui2-topo-lib/tsconfig.spec.json
new file mode 100644
index 0000000..16da33d
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/tsconfig.spec.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/spec",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "src/test.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/tslint.json b/web/gui2-topo-lib/projects/gui2-topo-lib/tslint.json
new file mode 100644
index 0000000..1db9f39
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/tslint.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tslint.json",
+ "rules": {
+ "directive-selector": [
+ true,
+ "attribute",
+ "onos",
+ "camelCase"
+ ],
+ "component-selector": [
+ true,
+ ["element", "attribute"],
+ "onos",
+ "kebab-case"
+ ]
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/protractor.conf.js b/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/protractor.conf.js
new file mode 100644
index 0000000..86776a3
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/protractor.conf.js
@@ -0,0 +1,28 @@
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/lib/config.ts
+
+const { SpecReporter } = require('jasmine-spec-reporter');
+
+exports.config = {
+ allScriptsTimeout: 11000,
+ specs: [
+ './src/**/*.e2e-spec.ts'
+ ],
+ capabilities: {
+ 'browserName': 'chrome'
+ },
+ directConnect: true,
+ baseUrl: 'http://localhost:4200/',
+ framework: 'jasmine',
+ jasmineNodeOpts: {
+ showColors: true,
+ defaultTimeoutInterval: 30000,
+ print: function() {}
+ },
+ onPrepare() {
+ require('ts-node').register({
+ project: require('path').join(__dirname, './tsconfig.e2e.json')
+ });
+ jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
+ }
+};
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/src/app.e2e-spec.ts b/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/src/app.e2e-spec.ts
new file mode 100644
index 0000000..56efac6
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/src/app.e2e-spec.ts
@@ -0,0 +1,14 @@
+import { AppPage } from './app.po';
+
+describe('workspace-project App', () => {
+ let page: AppPage;
+
+ beforeEach(() => {
+ page = new AppPage();
+ });
+
+ it('should display welcome message', () => {
+ page.navigateTo();
+ expect(page.getTitleText()).toEqual('Welcome to gui2-topo-tester!');
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/src/app.po.ts b/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/src/app.po.ts
new file mode 100644
index 0000000..72e463a
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/src/app.po.ts
@@ -0,0 +1,11 @@
+import { browser, by, element } from 'protractor';
+
+export class AppPage {
+ navigateTo() {
+ return browser.get('/');
+ }
+
+ getTitleText() {
+ return element(by.css('app-root h1')).getText();
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/tsconfig.e2e.json b/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/tsconfig.e2e.json
new file mode 100644
index 0000000..e3a479b
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester-e2e/tsconfig.e2e.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/app",
+ "module": "commonjs",
+ "target": "es5",
+ "types": [
+ "jasmine",
+ "jasminewd2",
+ "node"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/browserslist b/web/gui2-topo-lib/projects/gui2-topo-tester/browserslist
new file mode 100644
index 0000000..37371cb
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/browserslist
@@ -0,0 +1,11 @@
+# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+#
+# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
+
+> 0.5%
+last 2 versions
+Firefox ESR
+not dead
+not IE 9-11
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/karma.conf.js b/web/gui2-topo-lib/projects/gui2-topo-tester/karma.conf.js
new file mode 100644
index 0000000..b2417fd
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/karma.conf.js
@@ -0,0 +1,31 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma')
+ ],
+ client: {
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ dir: require('path').join(__dirname, '../../coverage'),
+ reports: ['html', 'lcovonly'],
+ fixWebpackSourcePaths: true
+ },
+ reporters: ['progress', 'kjhtml'],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['Chrome'],
+ singleRun: false
+ });
+};
\ No newline at end of file
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.css b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.css
new file mode 100644
index 0000000..e6f8352
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.css
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+#view h2 {
+ color: #3c3a3a;
+ /*margin: 32px 0 4px 16px;*/
+ padding: 0;
+ font-size: 18pt;
+ font-weight: lighter;
+}
+
+a {
+ color: #009fdb;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+a:visited {
+ color: #7fabdb;
+ text-decoration: none;
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.html b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.html
new file mode 100644
index 0000000..9293382
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.html
@@ -0,0 +1,7 @@
+<!--The content below is only a placeholder and can be replaced.-->
+<div style="text-align:center">
+ <p>ONOS Topology view test application</p>
+</div>
+<onos-topology [bannerHeight]="50"></onos-topology>
+
+
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.spec.ts
new file mode 100644
index 0000000..964800f
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.spec.ts
@@ -0,0 +1,31 @@
+import { TestBed, async } from '@angular/core/testing';
+import { AppComponent } from './app.component';
+
+describe('AppComponent', () => {
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ AppComponent
+ ],
+ }).compileComponents();
+ }));
+
+ it('should create the app', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ expect(app).toBeTruthy();
+ });
+
+ it(`should have as title 'gui2-topo-tester'`, () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ expect(app.title).toEqual('gui2-topo-tester');
+ });
+
+ it('should render title in a h1 tag', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.nativeElement;
+ expect(compiled.querySelector('h1').textContent).toContain('Welcome to gui2-topo-tester!');
+ });
+});
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.ts b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.ts
new file mode 100644
index 0000000..4261f48
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.component.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {AfterViewInit, Component, OnDestroy} from '@angular/core';
+import {
+ GlyphService,
+ KeysService,
+ LionService,
+ LogService,
+ ThemeService,
+ WebSocketService,
+ WsOptions
+} from 'gui2-fw-lib';
+import * as d3 from 'd3';
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.css']
+})
+export class AppComponent implements AfterViewInit, OnDestroy {
+ title = 'gui2-topo-tester';
+
+ constructor (
+ private lion: LionService,
+ private ts: ThemeService,
+ private gs: GlyphService,
+ private ks: KeysService,
+ public wss: WebSocketService,
+ private log: LogService,
+ ) {
+ // Testing of debugging
+ log.debug('OnosComponent: testing logger.debug()');
+ log.info('OnosComponent: testing logger.info()');
+ log.warn('OnosComponent: testing logger.warn()');
+ log.error('OnosComponent: testing logger.error()');
+
+ this.wss.createWebSocket(<WsOptions>{ wsport: 8181});
+
+ log.debug('OnosComponent constructed');
+ }
+
+ ngAfterViewInit(): void {
+ this.ks.installOn(d3.select('body'));
+ this.log.debug('AppComponent after view initialized');
+ }
+
+
+ ngOnDestroy() {
+ if (this.wss.isConnected()) {
+ this.log.debug('Stopping Web Socket connection');
+ this.wss.closeWebSocket();
+ }
+
+ this.log.debug('AppComponent destroyed');
+ }
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.module.ts b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.module.ts
new file mode 100644
index 0000000..2fbba74
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/app/app.module.ts
@@ -0,0 +1,33 @@
+import { BrowserModule } from '@angular/platform-browser';
+import { NgModule } from '@angular/core';
+
+import { AppComponent } from './app.component';
+import {ConsoleLoggerService, Gui2FwLibModule, LogService} from 'gui2-fw-lib';
+import {Gui2TopoLibModule} from 'gui2-topo-lib';
+import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
+import {RouterModule, Routes} from '@angular/router';
+import {HttpClientModule} from '@angular/common/http';
+
+const appRoutes: Routes = [
+ { path: '**', component: AppComponent }
+]
+
+@NgModule({
+ declarations: [
+ AppComponent
+ ],
+ imports: [
+ RouterModule.forRoot(appRoutes),
+ BrowserModule,
+ BrowserAnimationsModule,
+ Gui2FwLibModule,
+ Gui2TopoLibModule,
+ HttpClientModule
+ ],
+ providers: [
+ { provide: LogService, useClass: ConsoleLoggerService },
+ { provide: 'Window', useValue: window }
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule { }
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/assets/.gitkeep b/web/gui2-topo-lib/projects/gui2-topo-tester/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/assets/.gitkeep
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/assets/onos-logo.png b/web/gui2-topo-lib/projects/gui2-topo-tester/src/assets/onos-logo.png
new file mode 100644
index 0000000..8688cd6
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/assets/onos-logo.png
Binary files differ
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/environments/environment.prod.ts b/web/gui2-topo-lib/projects/gui2-topo-tester/src/environments/environment.prod.ts
new file mode 100644
index 0000000..3612073
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/environments/environment.prod.ts
@@ -0,0 +1,3 @@
+export const environment = {
+ production: true
+};
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/environments/environment.ts b/web/gui2-topo-lib/projects/gui2-topo-tester/src/environments/environment.ts
new file mode 100644
index 0000000..7b4f817
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/environments/environment.ts
@@ -0,0 +1,16 @@
+// This file can be replaced during build by using the `fileReplacements` array.
+// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
+// The list of file replacements can be found in `angular.json`.
+
+export const environment = {
+ production: false
+};
+
+/*
+ * For easier debugging in development mode, you can import the following file
+ * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
+ *
+ * This import should be commented out in production mode because it will have a negative impact
+ * on performance if an error is thrown.
+ */
+// import 'zone.js/dist/zone-error'; // Included with Angular CLI.
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/favicon.ico b/web/gui2-topo-lib/projects/gui2-topo-tester/src/favicon.ico
new file mode 100644
index 0000000..8081c7c
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/favicon.ico
Binary files differ
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/index.html b/web/gui2-topo-lib/projects/gui2-topo-tester/src/index.html
new file mode 100644
index 0000000..800bff9
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/index.html
@@ -0,0 +1,38 @@
+<!doctype html>
+<!--
+~ Copyright 2014-present Open Networking Foundation
+~
+~ Licensed under the Apache License, Version 2.0 (the "License");
+~ you may not use this file except in compliance with the License.
+~ You may obtain a copy of the License at
+~
+~ http://www.apache.org/licenses/LICENSE-2.0
+~
+~ Unless required by applicable law or agreed to in writing, software
+~ distributed under the License is distributed on an "AS IS" BASIS,
+~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+~ See the License for the specific language governing permissions and
+~ limitations under the License.
+-->
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <link rel="shortcut icon" href="data/img/onos-logo.png">
+
+ <link rel="apple-touch-icon" href="data/img/apple-touch-icon.png">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-status-bar-style" content="black">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+ <link href="app/fw/layer/loading.service.css" rel='stylesheet' type='text/css'>
+
+ <title>Gui2TopoTester</title>
+ <base href="/">
+
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
+</head>
+<body>
+ <app-root></app-root>
+</body>
+</html>
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/main.ts b/web/gui2-topo-lib/projects/gui2-topo-tester/src/main.ts
new file mode 100644
index 0000000..c7b673c
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/main.ts
@@ -0,0 +1,12 @@
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule)
+ .catch(err => console.error(err));
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/polyfills.ts b/web/gui2-topo-lib/projects/gui2-topo-tester/src/polyfills.ts
new file mode 100644
index 0000000..ee8b84d
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/polyfills.ts
@@ -0,0 +1,80 @@
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+
+/** IE9, IE10 and IE11 requires all of the following polyfills. **/
+// import 'core-js/es6/symbol';
+// import 'core-js/es6/object';
+// import 'core-js/es6/function';
+// import 'core-js/es6/parse-int';
+// import 'core-js/es6/parse-float';
+// import 'core-js/es6/number';
+// import 'core-js/es6/math';
+// import 'core-js/es6/string';
+// import 'core-js/es6/date';
+// import 'core-js/es6/array';
+// import 'core-js/es6/regexp';
+// import 'core-js/es6/map';
+// import 'core-js/es6/weak-map';
+// import 'core-js/es6/set';
+
+/**
+ * If the application will be indexed by Google Search, the following is required.
+ * Googlebot uses a renderer based on Chrome 41.
+ * https://developers.google.com/search/docs/guides/rendering
+ **/
+// import 'core-js/es6/array';
+
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+// import 'classlist.js'; // Run `npm install --save classlist.js`.
+
+/** IE10 and IE11 requires the following for the Reflect API. */
+// import 'core-js/es6/reflect';
+
+/**
+ * Web Animations `@angular/platform-browser/animations`
+ * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
+ * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
+ **/
+// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
+
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ */
+
+ // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+
+ /*
+ * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ * with the following flag, it will bypass `zone.js` patch for IE/Edge
+ */
+// (window as any).__Zone_enable_cross_context_check = true;
+
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js/dist/zone'; // Included with Angular CLI.
+
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/styles.css b/web/gui2-topo-lib/projects/gui2-topo-tester/src/styles.css
new file mode 100644
index 0000000..24801ba
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/styles.css
@@ -0,0 +1,18 @@
+/* You can add global styles to this file, and also import other style files */
+html {
+ font-family: 'Open Sans', normal;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ height: 100%;
+}
+
+/*
+ overflow hidden is to ensure that the body does not expand to account
+ for any flyout panes, that are positioned "off screen".
+ */
+body {
+ background-color: white;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/src/test.ts b/web/gui2-topo-lib/projects/gui2-topo-tester/src/test.ts
new file mode 100644
index 0000000..1631789
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/src/test.ts
@@ -0,0 +1,20 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/dist/zone-testing';
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: any;
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting()
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/tsconfig.app.json b/web/gui2-topo-lib/projects/gui2-topo-tester/tsconfig.app.json
new file mode 100644
index 0000000..bb16c46
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/tsconfig.app.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/app",
+ "types": []
+ },
+ "exclude": [
+ "test.ts",
+ "**/*.spec.ts"
+ ]
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/tsconfig.spec.json b/web/gui2-topo-lib/projects/gui2-topo-tester/tsconfig.spec.json
new file mode 100644
index 0000000..a809b0a
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/tsconfig.spec.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/spec",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "src/test.ts",
+ "src/polyfills.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/web/gui2-topo-lib/projects/gui2-topo-tester/tslint.json b/web/gui2-topo-lib/projects/gui2-topo-tester/tslint.json
new file mode 100644
index 0000000..0ad3cf4
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-tester/tslint.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tslint.json",
+ "rules": {
+ "directive-selector": [
+ true,
+ "attribute",
+ "app",
+ "camelCase"
+ ],
+ "component-selector": [
+ true,
+ "element",
+ "app",
+ "kebab-case"
+ ]
+ }
+}