blob: b0b4a0cf147eb1bbc87b7eac6c8103dfec738c16 [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/**
56 * ONOS GUI -- Topology Forces Graph Layer View.
Sean Condon0c577f62018-11-18 22:40:05 +000057 *
58 * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
59 * This drives the whole Force graph
Sean Condonf4f54a12018-10-10 23:25:46 +010060 */
61@Component({
62 selector: '[onos-forcesvg]',
63 templateUrl: './forcesvg.component.html',
Sean Condon0c577f62018-11-18 22:40:05 +000064 styleUrls: ['./forcesvg.component.css'],
65 changeDetection: ChangeDetectionStrategy.OnPush,
Sean Condonf4f54a12018-10-10 23:25:46 +010066})
Sean Condon0c577f62018-11-18 22:40:05 +000067export class ForceSvgComponent implements OnInit, OnChanges {
Sean Condonaa4366d2018-11-02 14:29:01 +000068 @Input() onosInstMastership: string = '';
Sean Condon0c577f62018-11-18 22:40:05 +000069 @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
70 @Output() linkSelected = new EventEmitter<RegionLink>();
Sean Condon50855cf2018-12-23 15:37:42 +000071 @Output() selectedNodeEvent = new EventEmitter<UiElement>();
Sean Condon0c577f62018-11-18 22:40:05 +000072 @Input() selectedLink: RegionLink = null;
Sean Condon021f0fa2018-12-06 23:31:11 -080073 @Input() showHosts: boolean = false;
Sean Condon91481822019-01-01 13:56:14 +000074 @Input() highlightPorts: boolean = true;
Sean Condon021f0fa2018-12-06 23:31:11 -080075 @Input() deviceLabelToggle: LabelToggle = LabelToggle.NONE;
76 @Input() hostLabelToggle: HostLabelToggle = HostLabelToggle.NONE;
Sean Condon0c577f62018-11-18 22:40:05 +000077 @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
Sean Condon021f0fa2018-12-06 23:31:11 -080078 private graph: ForceDirectedGraph;
Sean Condon0c577f62018-11-18 22:40:05 +000079 private _options: { width, height } = { width: 800, height: 600 };
Sean Condonf4f54a12018-10-10 23:25:46 +010080
Sean Condon021f0fa2018-12-06 23:31:11 -080081 // References to the children of this component - these are created in the
82 // template view with the *ngFor and we get them by a query here
Sean Condon0c577f62018-11-18 22:40:05 +000083 @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
Sean Condon021f0fa2018-12-06 23:31:11 -080084 @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
Sean Condon50855cf2018-12-23 15:37:42 +000085 @ViewChildren(LinkSvgComponent) links: QueryList<LinkSvgComponent>;
Sean Condonf4f54a12018-10-10 23:25:46 +010086
Sean Condon0c577f62018-11-18 22:40:05 +000087 constructor(
88 protected log: LogService,
Sean Condon0c577f62018-11-18 22:40:05 +000089 private ref: ChangeDetectorRef
90 ) {
91 this.selectedLink = null;
92 this.log.debug('ForceSvgComponent constructed');
93 }
94
95 /**
Sean Condon021f0fa2018-12-06 23:31:11 -080096 * Utility for extracting a node name from an endpoint string
97 * In some cases - have to remove the port number from the end of a device
98 * name
99 * @param endPtStr The end point name
100 */
101 private static extractNodeName(endPtStr: string): string {
102 const slash: number = endPtStr.indexOf('/');
103 if (slash === -1) {
104 return endPtStr;
105 } else {
106 const afterSlash = endPtStr.substr(slash + 1);
107 if (afterSlash === 'None') {
108 return endPtStr;
109 } else {
110 return endPtStr.substr(0, slash);
111 }
112 }
113 }
114
115 @HostListener('window:resize', ['$event'])
116 onResize(event) {
117 this.graph.initSimulation(this.options);
118 this.log.debug('Simulation reinit after resize', event);
119 }
120
121 /**
Sean Condon0c577f62018-11-18 22:40:05 +0000122 * After the component is initialized create the Force simulation
Sean Condon021f0fa2018-12-06 23:31:11 -0800123 * The list of devices, hosts and links will not have been receieved back
124 * from the WebSocket yet as this time - they will be updated later through
125 * ngOnChanges()
Sean Condon0c577f62018-11-18 22:40:05 +0000126 */
127 ngOnInit() {
128 // Receiving an initialized simulated graph from our custom d3 service
Sean Condon50855cf2018-12-23 15:37:42 +0000129 this.graph = new ForceDirectedGraph(this.options, this.log);
Sean Condon0c577f62018-11-18 22:40:05 +0000130
131 /** Binding change detection check on each tick
Sean Condon021f0fa2018-12-06 23:31:11 -0800132 * This along with an onPush change detection strategy should enforce
133 * checking only when relevant! This improves scripting computation
134 * duration in a couple of tests I've made, consistently. Also, it makes
135 * sense to avoid unnecessary checks when we are dealing only with
136 * simulations data binding.
Sean Condon0c577f62018-11-18 22:40:05 +0000137 */
138 this.graph.ticker.subscribe((simulation) => {
139 // this.log.debug("Force simulation has ticked", simulation);
140 this.ref.markForCheck();
141 });
142 this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
143
Sean Condon0c577f62018-11-18 22:40:05 +0000144 }
145
146 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800147 * When any one of the inputs get changed by a containing component, this
148 * gets called automatically. In addition this is called manually by
149 * topology.service when a response is received from the WebSocket from the
150 * server
Sean Condon0c577f62018-11-18 22:40:05 +0000151 *
152 * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
153 * The Links are added to the Link list of the simulation.
154 * Before they are added the Links are associated with Nodes based on their endPt
155 *
156 * @param changes - a list of changed @Input(s)
157 */
158 ngOnChanges(changes: SimpleChanges) {
159 if (changes['regionData']) {
160 const devices: Device[] =
161 changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
162 const hosts: Host[] =
163 changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
164 const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
165 this.graph.nodes = [];
166 if (devices) {
167 this.graph.nodes = devices;
168 }
169 if (hosts) {
170 this.graph.nodes = this.graph.nodes.concat(hosts);
171 }
172 if (subRegions) {
173 this.graph.nodes = this.graph.nodes.concat(subRegions);
174 }
175
176 // Associate the endpoints of each link with a real node
177 this.graph.links = [];
178 for (const linkIdx of Object.keys(this.regionData.links)) {
Sean Condon021f0fa2018-12-06 23:31:11 -0800179 const epA = ForceSvgComponent.extractNodeName(
180 this.regionData.links[linkIdx].epA);
Sean Condon0c577f62018-11-18 22:40:05 +0000181 this.regionData.links[linkIdx].source =
182 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800183 node.id === epA);
184 const epB = ForceSvgComponent.extractNodeName(
185 this.regionData.links[linkIdx].epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000186 this.regionData.links[linkIdx].target =
187 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800188 node.id === epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000189 this.regionData.links[linkIdx].index = Number(linkIdx);
190 }
191
192 this.graph.links = this.regionData.links;
193
194 this.graph.initSimulation(this.options);
195 this.graph.initNodes();
Sean Condon50855cf2018-12-23 15:37:42 +0000196 this.graph.initLinks();
Sean Condon0c577f62018-11-18 22:40:05 +0000197 this.log.debug('ForceSvgComponent input changed',
198 this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
199 }
Sean Condon021f0fa2018-12-06 23:31:11 -0800200
201 if (changes['showHosts']) {
202 this.showHosts = changes['showHosts'].currentValue;
203 }
204
Sean Condon91481822019-01-01 13:56:14 +0000205 if (changes['highlightPorts']) {
206 this.highlightPorts = changes['highlightPorts'].currentValue;
207 }
208
Sean Condon021f0fa2018-12-06 23:31:11 -0800209 // Pass on the changes to device
210 if (changes['deviceLabelToggle']) {
211 this.deviceLabelToggle = changes['deviceLabelToggle'].currentValue;
212 this.devices.forEach((d) => {
213 d.ngOnChanges({'labelToggle': changes['deviceLabelToggle']});
214 });
215 }
216
217 // Pass on the changes to host
218 if (changes['hostLabelToggle']) {
219 this.hostLabelToggle = changes['hostLabelToggle'].currentValue;
220 this.hosts.forEach((h) => {
221 h.ngOnChanges({'labelToggle': changes['hostLabelToggle']});
222 });
223 }
224
225 this.ref.markForCheck();
Sean Condon0c577f62018-11-18 22:40:05 +0000226 }
227
228 /**
229 * Get the index of LayerType so it can drive the visibility of nodes and
230 * hosts on layers
231 */
232 visibleLayerIdx(): number {
233 const layerKeys: string[] = Object.keys(LayerType);
234 for (const idx in layerKeys) {
235 if (LayerType[layerKeys[idx]] === this.visibleLayer) {
236 return Number(idx);
237 }
238 }
239 return -1;
240 }
241
242 selectLink(link: RegionLink): void {
243 this.selectedLink = link;
244 this.linkSelected.emit(link);
245 }
246
247 get options() {
248 return this._options = {
249 width: window.innerWidth,
250 height: window.innerHeight
251 };
252 }
253
Sean Condon021f0fa2018-12-06 23:31:11 -0800254 /**
255 * Iterate through all hosts and devices to deselect the previously selected
256 * node. The emit an event to the parent that lets it know the selection has
257 * changed.
258 * @param selectedNode the newly selected node
259 */
Sean Condon50855cf2018-12-23 15:37:42 +0000260 updateSelected(selectedNode: UiElement): void {
Sean Condon91481822019-01-01 13:56:14 +0000261 this.log.debug('Node or link selected', selectedNode ? selectedNode.id : 'none');
Sean Condon021f0fa2018-12-06 23:31:11 -0800262 this.devices
263 .filter((d) =>
264 selectedNode === undefined || d.device.id !== selectedNode.id)
265 .forEach((d) => d.deselect());
266 this.hosts
267 .filter((h) =>
268 selectedNode === undefined || h.host.id !== selectedNode.id)
269 .forEach((h) => h.deselect());
270
Sean Condon50855cf2018-12-23 15:37:42 +0000271 this.links
272 .filter((l) =>
273 selectedNode === undefined || l.link.id !== selectedNode.id)
274 .forEach((l) => l.deselect());
Sean Condon91481822019-01-01 13:56:14 +0000275 // Push the changes back up to parent (Topology Component)
Sean Condon021f0fa2018-12-06 23:31:11 -0800276 this.selectedNodeEvent.emit(selectedNode);
Sean Condon0c577f62018-11-18 22:40:05 +0000277 }
278
Sean Condon021f0fa2018-12-06 23:31:11 -0800279 /**
280 * We want to filter links to show only those not related to hosts if the
Sean Condon50855cf2018-12-23 15:37:42 +0000281 * 'showHosts' flag has been switched off. If 'showHosts' is true, then
Sean Condon021f0fa2018-12-06 23:31:11 -0800282 * display all links.
283 */
Sean Condon50855cf2018-12-23 15:37:42 +0000284 filteredLinks(): Link[] {
Sean Condon021f0fa2018-12-06 23:31:11 -0800285 return this.regionData.links.filter((h) =>
286 this.showHosts ||
287 ((<Host>h.source).nodeType !== 'host' &&
288 (<Host>h.target).nodeType !== 'host'));
Sean Condon0c577f62018-11-18 22:40:05 +0000289 }
Sean Condon50855cf2018-12-23 15:37:42 +0000290
291 /**
292 * When changes happen in the model, then model events are sent up through the
293 * Web Socket
294 * @param type - the type of the change
295 * @param memo - a qualifier on the type
296 * @param subject - the item that the update is for
297 * @param data - the new definition of the item
298 */
299 handleModelEvent(type: ModelEventType, memo: ModelEventMemo, subject: string, data: UiElement): void {
300 switch (type) {
301 case ModelEventType.DEVICE_ADDED_OR_UPDATED:
302 if (memo === ModelEventMemo.ADDED) {
303 this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
304 } else if (memo === ModelEventMemo.UPDATED) {
305 const oldDevice: Device =
306 this.regionData.devices[this.visibleLayerIdx()]
307 .find((d) => d.id === subject);
308 this.compareDevice(oldDevice, <Device>data);
309 } else {
310 this.log.warn('Device ', memo, ' - not yet implemented', data);
311 }
312 this.log.warn('Device ', memo, ' - not yet implemented', data);
313 break;
314 case ModelEventType.HOST_ADDED_OR_UPDATED:
315 if (memo === ModelEventMemo.ADDED) {
316 this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
317 this.log.warn('Host added - not yet implemented', data);
318 } else if (memo === ModelEventMemo.UPDATED) {
319 const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
320 .find((h) => h.id === subject);
321 this.compareHost(oldHost, <Host>data);
322 this.log.warn('Host updated - not yet implemented', data);
323 } else {
324 this.log.warn('Host change', memo, ' - unexpected');
325 }
326 break;
327 case ModelEventType.DEVICE_REMOVED:
328 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
329 const removeIdx: number =
330 this.regionData.devices[this.visibleLayerIdx()]
331 .findIndex((d) => d.id === subject);
332 const removeCmpt: DeviceNodeSvgComponent =
333 this.devices.find((dc) => dc.device.id === subject);
334 this.log.warn('Device ', subject, 'removed - not yet implemented', removeIdx, removeCmpt.device.id);
335 } else {
336 this.log.warn('Device removed - unexpected memo', memo);
337 }
338 break;
339 case ModelEventType.HOST_REMOVED:
340 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
341 const removeIdx: number =
342 this.regionData.hosts[this.visibleLayerIdx()]
343 .findIndex((h) => h.id === subject);
344 const removeCmpt: HostNodeSvgComponent =
345 this.hosts.find((hc) => hc.host.id === subject);
346 this.log.warn('Host ', subject, 'removed - not yet implemented', removeIdx, removeCmpt.host.id);
347 } else {
348 this.log.warn('Host removed - unexpected memo', memo);
349 }
350 break;
351 case ModelEventType.LINK_ADDED_OR_UPDATED:
352 this.log.warn('link added or updated - not yet implemented', subject);
353 break;
354 default:
355 this.log.error('Unexpected model event', type, 'for', subject);
356 }
357 }
358
359 private compareDevice(oldDevice: Device, updatedDevice: Device) {
360 if (oldDevice.master !== updatedDevice.master) {
361 this.log.debug('Mastership has changed for', updatedDevice.id, 'to', updatedDevice.master);
362 }
363 if (oldDevice.online !== updatedDevice.online) {
364 this.log.debug('Status has changed for', updatedDevice.id, 'to', updatedDevice.online);
365 }
366 }
367
368 private compareHost(oldHost: Host, updatedHost: Host) {
369 if (oldHost.configured !== updatedHost.configured) {
370 this.log.debug('Configured has changed for', updatedHost.id, 'to', updatedHost.configured);
371 }
372 }
373
374 /**
375 * When traffic monitoring is turned on (A key) highlights will be sent back
376 * from the WebSocket through the Traffic Service
377 * @param devices - an array of device highlights
378 * @param hosts - an array of host highlights
379 * @param links - an array of link highlights
380 */
381 handleHighlights(devices: Device[], hosts: Host[], links: LinkHighlight[]): void {
382
383 if (devices.length > 0) {
384 this.log.debug(devices.length, 'Devices highlighted');
385 devices.forEach((dh) => {
386 const deviceComponent: DeviceNodeSvgComponent = this.devices.find((d) => d.device.id === dh.id );
387 if (deviceComponent) {
388 deviceComponent.ngOnChanges(
389 {'deviceHighlight': new SimpleChange(<Device>{}, dh, true)}
390 );
391 this.log.debug('Highlighting device', deviceComponent.device.id);
392 } else {
393 this.log.warn('Device component not found', dh.id);
394 }
395 });
396 }
397 if (hosts.length > 0) {
398 this.log.debug(hosts.length, 'Hosts highlighted');
399 hosts.forEach((hh) => {
400 const hostComponent: HostNodeSvgComponent = this.hosts.find((h) => h.host.id === hh.id );
401 if (hostComponent) {
402 hostComponent.ngOnChanges(
403 {'hostHighlight': new SimpleChange(<Host>{}, hh, true)}
404 );
405 this.log.debug('Highlighting host', hostComponent.host.id);
406 }
407 });
408 }
409 if (links.length > 0) {
410 this.log.debug(links.length, 'Links highlighted');
411 links.forEach((lh) => {
412 const linkComponent: LinkSvgComponent = this.links.find((l) => l.link.id === lh.id );
413 if (linkComponent) { // A link might not be present is hosts viewing is switched off
414 linkComponent.ngOnChanges(
415 {'linkHighlight': new SimpleChange(<LinkHighlight>{}, lh, true)}
416 );
417 // this.log.debug('Highlighting link', linkComponent.link.id, lh.css, lh.label);
418 }
419 });
420 }
421 }
Sean Condonf4f54a12018-10-10 23:25:46 +0100422}
Sean Condon0c577f62018-11-18 22:40:05 +0000423