GUI2 Extract Topology view in to its own library
Change-Id: I45597d0902c99b5b3d606966866cc518011c54a0
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 + '%';
+ }
+}