blob: ebc70aa6e9617ea7da79ef2d6e8303aa574da738 [file] [log] [blame]
Sean Condonf4f54a12018-10-10 23:25:46 +01001/*
Sean Condon91481822019-01-01 13:56:14 +00002 * Copyright 2019-present Open Networking Foundation
Sean Condonf4f54a12018-10-10 23:25:46 +01003 *
4 * Licensed under the Apache License, Version 2.0 (the 'License');
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an 'AS IS' BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Sean Condon0c577f62018-11-18 22:40:05 +000016import {
17 ChangeDetectionStrategy,
18 ChangeDetectorRef,
19 Component,
20 EventEmitter,
21 HostListener,
22 Input,
23 OnChanges,
24 OnInit,
Sean Condon021f0fa2018-12-06 23:31:11 -080025 Output,
26 QueryList,
Sean Condon50855cf2018-12-23 15:37:42 +000027 SimpleChange,
Sean Condon021f0fa2018-12-06 23:31:11 -080028 SimpleChanges,
29 ViewChildren
Sean Condon0c577f62018-11-18 22:40:05 +000030} from '@angular/core';
Sean Condon1ae15802019-03-02 09:07:18 +000031import {
32 LocMeta,
33 LogService,
34 MetaUi,
35 WebSocketService,
36 ZoomUtils
37} from 'gui2-fw-lib';
Sean Condon0c577f62018-11-18 22:40:05 +000038import {
39 Device,
40 ForceDirectedGraph,
Sean Condon50855cf2018-12-23 15:37:42 +000041 Host,
42 HostLabelToggle,
Sean Condon0c577f62018-11-18 22:40:05 +000043 LabelToggle,
44 LayerType,
Sean Condon50855cf2018-12-23 15:37:42 +000045 Link,
46 LinkHighlight,
Sean Condon1ae15802019-03-02 09:07:18 +000047 Location,
Sean Condon50855cf2018-12-23 15:37:42 +000048 ModelEventMemo,
49 ModelEventType,
Sean Condon0c577f62018-11-18 22:40:05 +000050 Region,
51 RegionLink,
Sean Condon50855cf2018-12-23 15:37:42 +000052 SubRegion,
53 UiElement
Sean Condon0c577f62018-11-18 22:40:05 +000054} from './models';
Sean Condon50855cf2018-12-23 15:37:42 +000055import {
56 DeviceNodeSvgComponent,
57 HostNodeSvgComponent,
58 LinkSvgComponent
59} from './visuals';
Sean Condon71910542019-02-16 18:16:42 +000060import {
61 BackgroundSvgComponent,
62 LocationType
63} from '../backgroundsvg/backgroundsvg.component';
64
65interface UpdateMeta {
66 id: string;
67 class: string;
68 memento: MetaUi;
69}
Sean Condonaa4366d2018-11-02 14:29:01 +000070
Sean Condonf4f54a12018-10-10 23:25:46 +010071/**
72 * ONOS GUI -- Topology Forces Graph Layer View.
Sean Condon0c577f62018-11-18 22:40:05 +000073 *
74 * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
75 * This drives the whole Force graph
Sean Condonf4f54a12018-10-10 23:25:46 +010076 */
77@Component({
78 selector: '[onos-forcesvg]',
79 templateUrl: './forcesvg.component.html',
Sean Condon0c577f62018-11-18 22:40:05 +000080 styleUrls: ['./forcesvg.component.css'],
81 changeDetection: ChangeDetectionStrategy.OnPush,
Sean Condonf4f54a12018-10-10 23:25:46 +010082})
Sean Condon0c577f62018-11-18 22:40:05 +000083export class ForceSvgComponent implements OnInit, OnChanges {
Sean Condon021f0fa2018-12-06 23:31:11 -080084 @Input() deviceLabelToggle: LabelToggle = LabelToggle.NONE;
85 @Input() hostLabelToggle: HostLabelToggle = HostLabelToggle.NONE;
Sean Condonb2c483c2019-01-16 20:28:55 +000086 @Input() showHosts: boolean = false;
87 @Input() highlightPorts: boolean = true;
88 @Input() onosInstMastership: string = '';
89 @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
90 @Input() selectedLink: RegionLink = null;
Sean Condon1ae15802019-03-02 09:07:18 +000091 @Input() scale: number = 1;
Sean Condon0c577f62018-11-18 22:40:05 +000092 @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
Sean Condonb2c483c2019-01-16 20:28:55 +000093 @Output() linkSelected = new EventEmitter<RegionLink>();
94 @Output() selectedNodeEvent = new EventEmitter<UiElement>();
Sean Condon021f0fa2018-12-06 23:31:11 -080095 private graph: ForceDirectedGraph;
Sean Condon0c577f62018-11-18 22:40:05 +000096 private _options: { width, height } = { width: 800, height: 600 };
Sean Condonf4f54a12018-10-10 23:25:46 +010097
Sean Condon021f0fa2018-12-06 23:31:11 -080098 // References to the children of this component - these are created in the
99 // template view with the *ngFor and we get them by a query here
Sean Condon0c577f62018-11-18 22:40:05 +0000100 @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
Sean Condon021f0fa2018-12-06 23:31:11 -0800101 @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
Sean Condon50855cf2018-12-23 15:37:42 +0000102 @ViewChildren(LinkSvgComponent) links: QueryList<LinkSvgComponent>;
Sean Condonf4f54a12018-10-10 23:25:46 +0100103
Sean Condon0c577f62018-11-18 22:40:05 +0000104 constructor(
105 protected log: LogService,
Sean Condon71910542019-02-16 18:16:42 +0000106 private ref: ChangeDetectorRef,
107 protected wss: WebSocketService
Sean Condon0c577f62018-11-18 22:40:05 +0000108 ) {
109 this.selectedLink = null;
110 this.log.debug('ForceSvgComponent constructed');
111 }
112
113 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800114 * Utility for extracting a node name from an endpoint string
115 * In some cases - have to remove the port number from the end of a device
116 * name
117 * @param endPtStr The end point name
118 */
119 private static extractNodeName(endPtStr: string): string {
120 const slash: number = endPtStr.indexOf('/');
121 if (slash === -1) {
122 return endPtStr;
123 } else {
124 const afterSlash = endPtStr.substr(slash + 1);
125 if (afterSlash === 'None') {
126 return endPtStr;
127 } else {
128 return endPtStr.substr(0, slash);
129 }
130 }
131 }
132
133 @HostListener('window:resize', ['$event'])
134 onResize(event) {
135 this.graph.initSimulation(this.options);
136 this.log.debug('Simulation reinit after resize', event);
137 }
138
139 /**
Sean Condon0c577f62018-11-18 22:40:05 +0000140 * After the component is initialized create the Force simulation
Sean Condon021f0fa2018-12-06 23:31:11 -0800141 * The list of devices, hosts and links will not have been receieved back
142 * from the WebSocket yet as this time - they will be updated later through
143 * ngOnChanges()
Sean Condon0c577f62018-11-18 22:40:05 +0000144 */
145 ngOnInit() {
146 // Receiving an initialized simulated graph from our custom d3 service
Sean Condon50855cf2018-12-23 15:37:42 +0000147 this.graph = new ForceDirectedGraph(this.options, this.log);
Sean Condon0c577f62018-11-18 22:40:05 +0000148
149 /** Binding change detection check on each tick
Sean Condon021f0fa2018-12-06 23:31:11 -0800150 * This along with an onPush change detection strategy should enforce
151 * checking only when relevant! This improves scripting computation
152 * duration in a couple of tests I've made, consistently. Also, it makes
153 * sense to avoid unnecessary checks when we are dealing only with
154 * simulations data binding.
Sean Condon0c577f62018-11-18 22:40:05 +0000155 */
156 this.graph.ticker.subscribe((simulation) => {
157 // this.log.debug("Force simulation has ticked", simulation);
158 this.ref.markForCheck();
159 });
160 this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
161
Sean Condon0c577f62018-11-18 22:40:05 +0000162 }
163
164 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800165 * When any one of the inputs get changed by a containing component, this
166 * gets called automatically. In addition this is called manually by
167 * topology.service when a response is received from the WebSocket from the
168 * server
Sean Condon0c577f62018-11-18 22:40:05 +0000169 *
170 * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
171 * The Links are added to the Link list of the simulation.
172 * Before they are added the Links are associated with Nodes based on their endPt
173 *
174 * @param changes - a list of changed @Input(s)
175 */
176 ngOnChanges(changes: SimpleChanges) {
177 if (changes['regionData']) {
178 const devices: Device[] =
179 changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
180 const hosts: Host[] =
181 changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
182 const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
183 this.graph.nodes = [];
184 if (devices) {
185 this.graph.nodes = devices;
186 }
187 if (hosts) {
188 this.graph.nodes = this.graph.nodes.concat(hosts);
189 }
190 if (subRegions) {
191 this.graph.nodes = this.graph.nodes.concat(subRegions);
192 }
193
Sean Condon71910542019-02-16 18:16:42 +0000194 // If a node has a fixed location then assign it to fx and fy so
195 // that it doesn't get affected by forces
196 this.graph.nodes
197 .forEach((n) => {
198 const loc: Location = <Location>n['location'];
199 if (loc && loc.locType === LocationType.GEO) {
200 const position: MetaUi =
Sean Condon1ae15802019-03-02 09:07:18 +0000201 ZoomUtils.convertGeoToCanvas(
Sean Condon71910542019-02-16 18:16:42 +0000202 <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
203 n.fx = position.x;
204 n.fy = position.y;
205 this.log.debug('Found node', n.id, 'with', loc.locType);
206 }
207 });
208
Sean Condon0c577f62018-11-18 22:40:05 +0000209 // Associate the endpoints of each link with a real node
210 this.graph.links = [];
211 for (const linkIdx of Object.keys(this.regionData.links)) {
Sean Condon021f0fa2018-12-06 23:31:11 -0800212 const epA = ForceSvgComponent.extractNodeName(
213 this.regionData.links[linkIdx].epA);
Sean Condon0c577f62018-11-18 22:40:05 +0000214 this.regionData.links[linkIdx].source =
215 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800216 node.id === epA);
217 const epB = ForceSvgComponent.extractNodeName(
218 this.regionData.links[linkIdx].epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000219 this.regionData.links[linkIdx].target =
220 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800221 node.id === epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000222 this.regionData.links[linkIdx].index = Number(linkIdx);
223 }
224
225 this.graph.links = this.regionData.links;
226
227 this.graph.initSimulation(this.options);
228 this.graph.initNodes();
Sean Condon50855cf2018-12-23 15:37:42 +0000229 this.graph.initLinks();
Sean Condon0c577f62018-11-18 22:40:05 +0000230 this.log.debug('ForceSvgComponent input changed',
231 this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
232 }
Sean Condon021f0fa2018-12-06 23:31:11 -0800233
Sean Condon021f0fa2018-12-06 23:31:11 -0800234 this.ref.markForCheck();
Sean Condon0c577f62018-11-18 22:40:05 +0000235 }
236
237 /**
238 * Get the index of LayerType so it can drive the visibility of nodes and
239 * hosts on layers
240 */
241 visibleLayerIdx(): number {
242 const layerKeys: string[] = Object.keys(LayerType);
243 for (const idx in layerKeys) {
244 if (LayerType[layerKeys[idx]] === this.visibleLayer) {
245 return Number(idx);
246 }
247 }
248 return -1;
249 }
250
251 selectLink(link: RegionLink): void {
252 this.selectedLink = link;
253 this.linkSelected.emit(link);
254 }
255
256 get options() {
257 return this._options = {
258 width: window.innerWidth,
259 height: window.innerHeight
260 };
261 }
262
Sean Condon021f0fa2018-12-06 23:31:11 -0800263 /**
264 * Iterate through all hosts and devices to deselect the previously selected
265 * node. The emit an event to the parent that lets it know the selection has
266 * changed.
267 * @param selectedNode the newly selected node
268 */
Sean Condon50855cf2018-12-23 15:37:42 +0000269 updateSelected(selectedNode: UiElement): void {
Sean Condon91481822019-01-01 13:56:14 +0000270 this.log.debug('Node or link selected', selectedNode ? selectedNode.id : 'none');
Sean Condon021f0fa2018-12-06 23:31:11 -0800271 this.devices
272 .filter((d) =>
273 selectedNode === undefined || d.device.id !== selectedNode.id)
274 .forEach((d) => d.deselect());
275 this.hosts
276 .filter((h) =>
277 selectedNode === undefined || h.host.id !== selectedNode.id)
278 .forEach((h) => h.deselect());
279
Sean Condon50855cf2018-12-23 15:37:42 +0000280 this.links
281 .filter((l) =>
282 selectedNode === undefined || l.link.id !== selectedNode.id)
283 .forEach((l) => l.deselect());
Sean Condon91481822019-01-01 13:56:14 +0000284 // Push the changes back up to parent (Topology Component)
Sean Condon021f0fa2018-12-06 23:31:11 -0800285 this.selectedNodeEvent.emit(selectedNode);
Sean Condon0c577f62018-11-18 22:40:05 +0000286 }
287
Sean Condon021f0fa2018-12-06 23:31:11 -0800288 /**
289 * We want to filter links to show only those not related to hosts if the
Sean Condon50855cf2018-12-23 15:37:42 +0000290 * 'showHosts' flag has been switched off. If 'showHosts' is true, then
Sean Condon021f0fa2018-12-06 23:31:11 -0800291 * display all links.
292 */
Sean Condon50855cf2018-12-23 15:37:42 +0000293 filteredLinks(): Link[] {
Sean Condon021f0fa2018-12-06 23:31:11 -0800294 return this.regionData.links.filter((h) =>
295 this.showHosts ||
296 ((<Host>h.source).nodeType !== 'host' &&
297 (<Host>h.target).nodeType !== 'host'));
Sean Condon0c577f62018-11-18 22:40:05 +0000298 }
Sean Condon50855cf2018-12-23 15:37:42 +0000299
300 /**
301 * When changes happen in the model, then model events are sent up through the
302 * Web Socket
303 * @param type - the type of the change
304 * @param memo - a qualifier on the type
305 * @param subject - the item that the update is for
306 * @param data - the new definition of the item
307 */
308 handleModelEvent(type: ModelEventType, memo: ModelEventMemo, subject: string, data: UiElement): void {
309 switch (type) {
310 case ModelEventType.DEVICE_ADDED_OR_UPDATED:
311 if (memo === ModelEventMemo.ADDED) {
312 this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
313 } else if (memo === ModelEventMemo.UPDATED) {
314 const oldDevice: Device =
315 this.regionData.devices[this.visibleLayerIdx()]
316 .find((d) => d.id === subject);
317 this.compareDevice(oldDevice, <Device>data);
318 } else {
319 this.log.warn('Device ', memo, ' - not yet implemented', data);
320 }
321 this.log.warn('Device ', memo, ' - not yet implemented', data);
322 break;
323 case ModelEventType.HOST_ADDED_OR_UPDATED:
324 if (memo === ModelEventMemo.ADDED) {
325 this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
326 this.log.warn('Host added - not yet implemented', data);
327 } else if (memo === ModelEventMemo.UPDATED) {
328 const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
329 .find((h) => h.id === subject);
330 this.compareHost(oldHost, <Host>data);
331 this.log.warn('Host updated - not yet implemented', data);
332 } else {
333 this.log.warn('Host change', memo, ' - unexpected');
334 }
335 break;
336 case ModelEventType.DEVICE_REMOVED:
337 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
338 const removeIdx: number =
339 this.regionData.devices[this.visibleLayerIdx()]
340 .findIndex((d) => d.id === subject);
341 const removeCmpt: DeviceNodeSvgComponent =
342 this.devices.find((dc) => dc.device.id === subject);
343 this.log.warn('Device ', subject, 'removed - not yet implemented', removeIdx, removeCmpt.device.id);
344 } else {
345 this.log.warn('Device removed - unexpected memo', memo);
346 }
347 break;
348 case ModelEventType.HOST_REMOVED:
349 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
350 const removeIdx: number =
351 this.regionData.hosts[this.visibleLayerIdx()]
352 .findIndex((h) => h.id === subject);
353 const removeCmpt: HostNodeSvgComponent =
354 this.hosts.find((hc) => hc.host.id === subject);
355 this.log.warn('Host ', subject, 'removed - not yet implemented', removeIdx, removeCmpt.host.id);
356 } else {
357 this.log.warn('Host removed - unexpected memo', memo);
358 }
359 break;
360 case ModelEventType.LINK_ADDED_OR_UPDATED:
361 this.log.warn('link added or updated - not yet implemented', subject);
362 break;
363 default:
364 this.log.error('Unexpected model event', type, 'for', subject);
365 }
366 }
367
368 private compareDevice(oldDevice: Device, updatedDevice: Device) {
369 if (oldDevice.master !== updatedDevice.master) {
370 this.log.debug('Mastership has changed for', updatedDevice.id, 'to', updatedDevice.master);
371 }
372 if (oldDevice.online !== updatedDevice.online) {
373 this.log.debug('Status has changed for', updatedDevice.id, 'to', updatedDevice.online);
374 }
375 }
376
377 private compareHost(oldHost: Host, updatedHost: Host) {
378 if (oldHost.configured !== updatedHost.configured) {
379 this.log.debug('Configured has changed for', updatedHost.id, 'to', updatedHost.configured);
380 }
381 }
382
383 /**
384 * When traffic monitoring is turned on (A key) highlights will be sent back
385 * from the WebSocket through the Traffic Service
386 * @param devices - an array of device highlights
387 * @param hosts - an array of host highlights
388 * @param links - an array of link highlights
389 */
390 handleHighlights(devices: Device[], hosts: Host[], links: LinkHighlight[]): void {
391
392 if (devices.length > 0) {
393 this.log.debug(devices.length, 'Devices highlighted');
394 devices.forEach((dh) => {
395 const deviceComponent: DeviceNodeSvgComponent = this.devices.find((d) => d.device.id === dh.id );
396 if (deviceComponent) {
397 deviceComponent.ngOnChanges(
398 {'deviceHighlight': new SimpleChange(<Device>{}, dh, true)}
399 );
400 this.log.debug('Highlighting device', deviceComponent.device.id);
401 } else {
402 this.log.warn('Device component not found', dh.id);
403 }
404 });
405 }
406 if (hosts.length > 0) {
407 this.log.debug(hosts.length, 'Hosts highlighted');
408 hosts.forEach((hh) => {
409 const hostComponent: HostNodeSvgComponent = this.hosts.find((h) => h.host.id === hh.id );
410 if (hostComponent) {
411 hostComponent.ngOnChanges(
412 {'hostHighlight': new SimpleChange(<Host>{}, hh, true)}
413 );
414 this.log.debug('Highlighting host', hostComponent.host.id);
415 }
416 });
417 }
418 if (links.length > 0) {
419 this.log.debug(links.length, 'Links highlighted');
420 links.forEach((lh) => {
421 const linkComponent: LinkSvgComponent = this.links.find((l) => l.link.id === lh.id );
422 if (linkComponent) { // A link might not be present is hosts viewing is switched off
423 linkComponent.ngOnChanges(
424 {'linkHighlight': new SimpleChange(<LinkHighlight>{}, lh, true)}
425 );
426 // this.log.debug('Highlighting link', linkComponent.link.id, lh.css, lh.label);
427 }
428 });
429 }
430 }
Sean Condon71910542019-02-16 18:16:42 +0000431
432 /**
433 * As nodes are dragged around the graph, their new location should be sent
434 * back to server
435 * @param klass The class of node e.g. 'host' or 'device'
436 * @param id - the ID of the node
437 * @param newLocation - the new Location of the node
438 */
439 nodeMoved(klass: string, id: string, newLocation: MetaUi) {
440 this.wss.sendEvent('updateMeta', <UpdateMeta>{
441 id: id,
442 class: klass,
443 memento: newLocation
444 });
445 this.log.debug(klass, id, 'has been moved to', newLocation);
446 }
Sean Condon1ae15802019-03-02 09:07:18 +0000447
448 resetNodeLocations() {
449 this.devices.forEach((d) => {
450 d.resetNodeLocation();
451 });
452 }
Sean Condonf4f54a12018-10-10 23:25:46 +0100453}
Sean Condon0c577f62018-11-18 22:40:05 +0000454