blob: d2a0b18663c55ce69b375c431d987a249c6871d2 [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 Condon91481822019-01-01 13:56:14 +000031import {LogService} from 'gui2-fw-lib';
Sean Condon0c577f62018-11-18 22:40:05 +000032import {
33 Device,
34 ForceDirectedGraph,
Sean Condon50855cf2018-12-23 15:37:42 +000035 Host,
36 HostLabelToggle,
Sean Condon0c577f62018-11-18 22:40:05 +000037 LabelToggle,
38 LayerType,
Sean Condon50855cf2018-12-23 15:37:42 +000039 Link,
40 LinkHighlight,
41 ModelEventMemo,
42 ModelEventType,
Sean Condon0c577f62018-11-18 22:40:05 +000043 Region,
44 RegionLink,
Sean Condon50855cf2018-12-23 15:37:42 +000045 SubRegion,
46 UiElement
Sean Condon0c577f62018-11-18 22:40:05 +000047} from './models';
Sean Condon50855cf2018-12-23 15:37:42 +000048import {
49 DeviceNodeSvgComponent,
50 HostNodeSvgComponent,
51 LinkSvgComponent
52} from './visuals';
Sean Condonaa4366d2018-11-02 14:29:01 +000053
Sean Condonf4f54a12018-10-10 23:25:46 +010054/**
55 * ONOS GUI -- Topology Forces Graph Layer View.
Sean Condon0c577f62018-11-18 22:40:05 +000056 *
57 * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
58 * This drives the whole Force graph
Sean Condonf4f54a12018-10-10 23:25:46 +010059 */
60@Component({
61 selector: '[onos-forcesvg]',
62 templateUrl: './forcesvg.component.html',
Sean Condon0c577f62018-11-18 22:40:05 +000063 styleUrls: ['./forcesvg.component.css'],
64 changeDetection: ChangeDetectionStrategy.OnPush,
Sean Condonf4f54a12018-10-10 23:25:46 +010065})
Sean Condon0c577f62018-11-18 22:40:05 +000066export class ForceSvgComponent implements OnInit, OnChanges {
Sean Condon021f0fa2018-12-06 23:31:11 -080067 @Input() deviceLabelToggle: LabelToggle = LabelToggle.NONE;
68 @Input() hostLabelToggle: HostLabelToggle = HostLabelToggle.NONE;
Sean Condonb2c483c2019-01-16 20:28:55 +000069 @Input() showHosts: boolean = false;
70 @Input() highlightPorts: boolean = true;
71 @Input() onosInstMastership: string = '';
72 @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
73 @Input() selectedLink: RegionLink = null;
Sean Condon0c577f62018-11-18 22:40:05 +000074 @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
Sean Condonb2c483c2019-01-16 20:28:55 +000075 @Output() linkSelected = new EventEmitter<RegionLink>();
76 @Output() selectedNodeEvent = new EventEmitter<UiElement>();
Sean Condon021f0fa2018-12-06 23:31:11 -080077 private graph: ForceDirectedGraph;
Sean Condon0c577f62018-11-18 22:40:05 +000078 private _options: { width, height } = { width: 800, height: 600 };
Sean Condonf4f54a12018-10-10 23:25:46 +010079
Sean Condon021f0fa2018-12-06 23:31:11 -080080 // References to the children of this component - these are created in the
81 // template view with the *ngFor and we get them by a query here
Sean Condon0c577f62018-11-18 22:40:05 +000082 @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
Sean Condon021f0fa2018-12-06 23:31:11 -080083 @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
Sean Condon50855cf2018-12-23 15:37:42 +000084 @ViewChildren(LinkSvgComponent) links: QueryList<LinkSvgComponent>;
Sean Condonf4f54a12018-10-10 23:25:46 +010085
Sean Condon0c577f62018-11-18 22:40:05 +000086 constructor(
87 protected log: LogService,
Sean Condon0c577f62018-11-18 22:40:05 +000088 private ref: ChangeDetectorRef
89 ) {
90 this.selectedLink = null;
91 this.log.debug('ForceSvgComponent constructed');
92 }
93
94 /**
Sean Condon021f0fa2018-12-06 23:31:11 -080095 * Utility for extracting a node name from an endpoint string
96 * In some cases - have to remove the port number from the end of a device
97 * name
98 * @param endPtStr The end point name
99 */
100 private static extractNodeName(endPtStr: string): string {
101 const slash: number = endPtStr.indexOf('/');
102 if (slash === -1) {
103 return endPtStr;
104 } else {
105 const afterSlash = endPtStr.substr(slash + 1);
106 if (afterSlash === 'None') {
107 return endPtStr;
108 } else {
109 return endPtStr.substr(0, slash);
110 }
111 }
112 }
113
114 @HostListener('window:resize', ['$event'])
115 onResize(event) {
116 this.graph.initSimulation(this.options);
117 this.log.debug('Simulation reinit after resize', event);
118 }
119
120 /**
Sean Condon0c577f62018-11-18 22:40:05 +0000121 * After the component is initialized create the Force simulation
Sean Condon021f0fa2018-12-06 23:31:11 -0800122 * The list of devices, hosts and links will not have been receieved back
123 * from the WebSocket yet as this time - they will be updated later through
124 * ngOnChanges()
Sean Condon0c577f62018-11-18 22:40:05 +0000125 */
126 ngOnInit() {
127 // Receiving an initialized simulated graph from our custom d3 service
Sean Condon50855cf2018-12-23 15:37:42 +0000128 this.graph = new ForceDirectedGraph(this.options, this.log);
Sean Condon0c577f62018-11-18 22:40:05 +0000129
130 /** Binding change detection check on each tick
Sean Condon021f0fa2018-12-06 23:31:11 -0800131 * This along with an onPush change detection strategy should enforce
132 * checking only when relevant! This improves scripting computation
133 * duration in a couple of tests I've made, consistently. Also, it makes
134 * sense to avoid unnecessary checks when we are dealing only with
135 * simulations data binding.
Sean Condon0c577f62018-11-18 22:40:05 +0000136 */
137 this.graph.ticker.subscribe((simulation) => {
138 // this.log.debug("Force simulation has ticked", simulation);
139 this.ref.markForCheck();
140 });
141 this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
142
Sean Condon0c577f62018-11-18 22:40:05 +0000143 }
144
145 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800146 * When any one of the inputs get changed by a containing component, this
147 * gets called automatically. In addition this is called manually by
148 * topology.service when a response is received from the WebSocket from the
149 * server
Sean Condon0c577f62018-11-18 22:40:05 +0000150 *
151 * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
152 * The Links are added to the Link list of the simulation.
153 * Before they are added the Links are associated with Nodes based on their endPt
154 *
155 * @param changes - a list of changed @Input(s)
156 */
157 ngOnChanges(changes: SimpleChanges) {
158 if (changes['regionData']) {
159 const devices: Device[] =
160 changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
161 const hosts: Host[] =
162 changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
163 const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
164 this.graph.nodes = [];
165 if (devices) {
166 this.graph.nodes = devices;
167 }
168 if (hosts) {
169 this.graph.nodes = this.graph.nodes.concat(hosts);
170 }
171 if (subRegions) {
172 this.graph.nodes = this.graph.nodes.concat(subRegions);
173 }
174
175 // Associate the endpoints of each link with a real node
176 this.graph.links = [];
177 for (const linkIdx of Object.keys(this.regionData.links)) {
Sean Condon021f0fa2018-12-06 23:31:11 -0800178 const epA = ForceSvgComponent.extractNodeName(
179 this.regionData.links[linkIdx].epA);
Sean Condon0c577f62018-11-18 22:40:05 +0000180 this.regionData.links[linkIdx].source =
181 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800182 node.id === epA);
183 const epB = ForceSvgComponent.extractNodeName(
184 this.regionData.links[linkIdx].epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000185 this.regionData.links[linkIdx].target =
186 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800187 node.id === epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000188 this.regionData.links[linkIdx].index = Number(linkIdx);
189 }
190
191 this.graph.links = this.regionData.links;
192
193 this.graph.initSimulation(this.options);
194 this.graph.initNodes();
Sean Condon50855cf2018-12-23 15:37:42 +0000195 this.graph.initLinks();
Sean Condon0c577f62018-11-18 22:40:05 +0000196 this.log.debug('ForceSvgComponent input changed',
197 this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
198 }
Sean Condon021f0fa2018-12-06 23:31:11 -0800199
Sean Condon021f0fa2018-12-06 23:31:11 -0800200 this.ref.markForCheck();
Sean Condon0c577f62018-11-18 22:40:05 +0000201 }
202
203 /**
204 * Get the index of LayerType so it can drive the visibility of nodes and
205 * hosts on layers
206 */
207 visibleLayerIdx(): number {
208 const layerKeys: string[] = Object.keys(LayerType);
209 for (const idx in layerKeys) {
210 if (LayerType[layerKeys[idx]] === this.visibleLayer) {
211 return Number(idx);
212 }
213 }
214 return -1;
215 }
216
217 selectLink(link: RegionLink): void {
218 this.selectedLink = link;
219 this.linkSelected.emit(link);
220 }
221
222 get options() {
223 return this._options = {
224 width: window.innerWidth,
225 height: window.innerHeight
226 };
227 }
228
Sean Condon021f0fa2018-12-06 23:31:11 -0800229 /**
230 * Iterate through all hosts and devices to deselect the previously selected
231 * node. The emit an event to the parent that lets it know the selection has
232 * changed.
233 * @param selectedNode the newly selected node
234 */
Sean Condon50855cf2018-12-23 15:37:42 +0000235 updateSelected(selectedNode: UiElement): void {
Sean Condon91481822019-01-01 13:56:14 +0000236 this.log.debug('Node or link selected', selectedNode ? selectedNode.id : 'none');
Sean Condon021f0fa2018-12-06 23:31:11 -0800237 this.devices
238 .filter((d) =>
239 selectedNode === undefined || d.device.id !== selectedNode.id)
240 .forEach((d) => d.deselect());
241 this.hosts
242 .filter((h) =>
243 selectedNode === undefined || h.host.id !== selectedNode.id)
244 .forEach((h) => h.deselect());
245
Sean Condon50855cf2018-12-23 15:37:42 +0000246 this.links
247 .filter((l) =>
248 selectedNode === undefined || l.link.id !== selectedNode.id)
249 .forEach((l) => l.deselect());
Sean Condon91481822019-01-01 13:56:14 +0000250 // Push the changes back up to parent (Topology Component)
Sean Condon021f0fa2018-12-06 23:31:11 -0800251 this.selectedNodeEvent.emit(selectedNode);
Sean Condon0c577f62018-11-18 22:40:05 +0000252 }
253
Sean Condon021f0fa2018-12-06 23:31:11 -0800254 /**
255 * We want to filter links to show only those not related to hosts if the
Sean Condon50855cf2018-12-23 15:37:42 +0000256 * 'showHosts' flag has been switched off. If 'showHosts' is true, then
Sean Condon021f0fa2018-12-06 23:31:11 -0800257 * display all links.
258 */
Sean Condon50855cf2018-12-23 15:37:42 +0000259 filteredLinks(): Link[] {
Sean Condon021f0fa2018-12-06 23:31:11 -0800260 return this.regionData.links.filter((h) =>
261 this.showHosts ||
262 ((<Host>h.source).nodeType !== 'host' &&
263 (<Host>h.target).nodeType !== 'host'));
Sean Condon0c577f62018-11-18 22:40:05 +0000264 }
Sean Condon50855cf2018-12-23 15:37:42 +0000265
266 /**
267 * When changes happen in the model, then model events are sent up through the
268 * Web Socket
269 * @param type - the type of the change
270 * @param memo - a qualifier on the type
271 * @param subject - the item that the update is for
272 * @param data - the new definition of the item
273 */
274 handleModelEvent(type: ModelEventType, memo: ModelEventMemo, subject: string, data: UiElement): void {
275 switch (type) {
276 case ModelEventType.DEVICE_ADDED_OR_UPDATED:
277 if (memo === ModelEventMemo.ADDED) {
278 this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
279 } else if (memo === ModelEventMemo.UPDATED) {
280 const oldDevice: Device =
281 this.regionData.devices[this.visibleLayerIdx()]
282 .find((d) => d.id === subject);
283 this.compareDevice(oldDevice, <Device>data);
284 } else {
285 this.log.warn('Device ', memo, ' - not yet implemented', data);
286 }
287 this.log.warn('Device ', memo, ' - not yet implemented', data);
288 break;
289 case ModelEventType.HOST_ADDED_OR_UPDATED:
290 if (memo === ModelEventMemo.ADDED) {
291 this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
292 this.log.warn('Host added - not yet implemented', data);
293 } else if (memo === ModelEventMemo.UPDATED) {
294 const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
295 .find((h) => h.id === subject);
296 this.compareHost(oldHost, <Host>data);
297 this.log.warn('Host updated - not yet implemented', data);
298 } else {
299 this.log.warn('Host change', memo, ' - unexpected');
300 }
301 break;
302 case ModelEventType.DEVICE_REMOVED:
303 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
304 const removeIdx: number =
305 this.regionData.devices[this.visibleLayerIdx()]
306 .findIndex((d) => d.id === subject);
307 const removeCmpt: DeviceNodeSvgComponent =
308 this.devices.find((dc) => dc.device.id === subject);
309 this.log.warn('Device ', subject, 'removed - not yet implemented', removeIdx, removeCmpt.device.id);
310 } else {
311 this.log.warn('Device removed - unexpected memo', memo);
312 }
313 break;
314 case ModelEventType.HOST_REMOVED:
315 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
316 const removeIdx: number =
317 this.regionData.hosts[this.visibleLayerIdx()]
318 .findIndex((h) => h.id === subject);
319 const removeCmpt: HostNodeSvgComponent =
320 this.hosts.find((hc) => hc.host.id === subject);
321 this.log.warn('Host ', subject, 'removed - not yet implemented', removeIdx, removeCmpt.host.id);
322 } else {
323 this.log.warn('Host removed - unexpected memo', memo);
324 }
325 break;
326 case ModelEventType.LINK_ADDED_OR_UPDATED:
327 this.log.warn('link added or updated - not yet implemented', subject);
328 break;
329 default:
330 this.log.error('Unexpected model event', type, 'for', subject);
331 }
332 }
333
334 private compareDevice(oldDevice: Device, updatedDevice: Device) {
335 if (oldDevice.master !== updatedDevice.master) {
336 this.log.debug('Mastership has changed for', updatedDevice.id, 'to', updatedDevice.master);
337 }
338 if (oldDevice.online !== updatedDevice.online) {
339 this.log.debug('Status has changed for', updatedDevice.id, 'to', updatedDevice.online);
340 }
341 }
342
343 private compareHost(oldHost: Host, updatedHost: Host) {
344 if (oldHost.configured !== updatedHost.configured) {
345 this.log.debug('Configured has changed for', updatedHost.id, 'to', updatedHost.configured);
346 }
347 }
348
349 /**
350 * When traffic monitoring is turned on (A key) highlights will be sent back
351 * from the WebSocket through the Traffic Service
352 * @param devices - an array of device highlights
353 * @param hosts - an array of host highlights
354 * @param links - an array of link highlights
355 */
356 handleHighlights(devices: Device[], hosts: Host[], links: LinkHighlight[]): void {
357
358 if (devices.length > 0) {
359 this.log.debug(devices.length, 'Devices highlighted');
360 devices.forEach((dh) => {
361 const deviceComponent: DeviceNodeSvgComponent = this.devices.find((d) => d.device.id === dh.id );
362 if (deviceComponent) {
363 deviceComponent.ngOnChanges(
364 {'deviceHighlight': new SimpleChange(<Device>{}, dh, true)}
365 );
366 this.log.debug('Highlighting device', deviceComponent.device.id);
367 } else {
368 this.log.warn('Device component not found', dh.id);
369 }
370 });
371 }
372 if (hosts.length > 0) {
373 this.log.debug(hosts.length, 'Hosts highlighted');
374 hosts.forEach((hh) => {
375 const hostComponent: HostNodeSvgComponent = this.hosts.find((h) => h.host.id === hh.id );
376 if (hostComponent) {
377 hostComponent.ngOnChanges(
378 {'hostHighlight': new SimpleChange(<Host>{}, hh, true)}
379 );
380 this.log.debug('Highlighting host', hostComponent.host.id);
381 }
382 });
383 }
384 if (links.length > 0) {
385 this.log.debug(links.length, 'Links highlighted');
386 links.forEach((lh) => {
387 const linkComponent: LinkSvgComponent = this.links.find((l) => l.link.id === lh.id );
388 if (linkComponent) { // A link might not be present is hosts viewing is switched off
389 linkComponent.ngOnChanges(
390 {'linkHighlight': new SimpleChange(<LinkHighlight>{}, lh, true)}
391 );
392 // this.log.debug('Highlighting link', linkComponent.link.id, lh.css, lh.label);
393 }
394 });
395 }
396 }
Sean Condonf4f54a12018-10-10 23:25:46 +0100397}
Sean Condon0c577f62018-11-18 22:40:05 +0000398