blob: 843c19c9805df2b93c268e2b54ea71f3d04c1c3e [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 {
Sean Condon28884332019-03-21 14:07:00 +000039 Device, DeviceProps,
Sean Condon0c577f62018-11-18 22:40:05 +000040 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 Condon28884332019-03-21 14:07:00 +000050 Node,
Sean Condon0c577f62018-11-18 22:40:05 +000051 Region,
52 RegionLink,
Sean Condon50855cf2018-12-23 15:37:42 +000053 SubRegion,
54 UiElement
Sean Condon0c577f62018-11-18 22:40:05 +000055} from './models';
Sean Condonee545762019-03-09 10:43:58 +000056import {LocationType} from '../backgroundsvg/backgroundsvg.component';
Sean Condonff85fbe2019-03-16 14:28:46 +000057import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
58import { HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
59import { LinkSvgComponent} from './visuals/linksvg/linksvg.component';
Sean Condon28884332019-03-21 14:07:00 +000060import { Options } from './models/force-directed-graph';
Sean Condon71910542019-02-16 18:16:42 +000061
62interface UpdateMeta {
63 id: string;
64 class: string;
65 memento: MetaUi;
66}
Sean Condonaa4366d2018-11-02 14:29:01 +000067
Sean Condon28884332019-03-21 14:07:00 +000068const SVGCANVAS = <Options>{
69 width: 1000,
70 height: 1000
71};
72
73interface ChangeSummary {
74 numChanges: number;
75 locationChanged: boolean;
76}
77
Sean Condonf4f54a12018-10-10 23:25:46 +010078/**
79 * ONOS GUI -- Topology Forces Graph Layer View.
Sean Condon0c577f62018-11-18 22:40:05 +000080 *
81 * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
82 * This drives the whole Force graph
Sean Condonf4f54a12018-10-10 23:25:46 +010083 */
84@Component({
85 selector: '[onos-forcesvg]',
86 templateUrl: './forcesvg.component.html',
Sean Condon0c577f62018-11-18 22:40:05 +000087 styleUrls: ['./forcesvg.component.css'],
88 changeDetection: ChangeDetectionStrategy.OnPush,
Sean Condonf4f54a12018-10-10 23:25:46 +010089})
Sean Condon0c577f62018-11-18 22:40:05 +000090export class ForceSvgComponent implements OnInit, OnChanges {
Sean Condonff85fbe2019-03-16 14:28:46 +000091 @Input() deviceLabelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
92 @Input() hostLabelToggle: HostLabelToggle.Enum = HostLabelToggle.Enum.NONE;
Sean Condonb2c483c2019-01-16 20:28:55 +000093 @Input() showHosts: boolean = false;
94 @Input() highlightPorts: boolean = true;
95 @Input() onosInstMastership: string = '';
96 @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
97 @Input() selectedLink: RegionLink = null;
Sean Condon1ae15802019-03-02 09:07:18 +000098 @Input() scale: number = 1;
Sean Condon0c577f62018-11-18 22:40:05 +000099 @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
Sean Condonb2c483c2019-01-16 20:28:55 +0000100 @Output() linkSelected = new EventEmitter<RegionLink>();
101 @Output() selectedNodeEvent = new EventEmitter<UiElement>();
Sean Condonff85fbe2019-03-16 14:28:46 +0000102 public graph: ForceDirectedGraph;
Sean Condonf4f54a12018-10-10 23:25:46 +0100103
Sean Condon021f0fa2018-12-06 23:31:11 -0800104 // References to the children of this component - these are created in the
105 // template view with the *ngFor and we get them by a query here
Sean Condon0c577f62018-11-18 22:40:05 +0000106 @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
Sean Condon021f0fa2018-12-06 23:31:11 -0800107 @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
Sean Condon50855cf2018-12-23 15:37:42 +0000108 @ViewChildren(LinkSvgComponent) links: QueryList<LinkSvgComponent>;
Sean Condonf4f54a12018-10-10 23:25:46 +0100109
Sean Condon0c577f62018-11-18 22:40:05 +0000110 constructor(
111 protected log: LogService,
Sean Condon71910542019-02-16 18:16:42 +0000112 private ref: ChangeDetectorRef,
113 protected wss: WebSocketService
Sean Condon0c577f62018-11-18 22:40:05 +0000114 ) {
115 this.selectedLink = null;
116 this.log.debug('ForceSvgComponent constructed');
117 }
118
119 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800120 * Utility for extracting a node name from an endpoint string
121 * In some cases - have to remove the port number from the end of a device
122 * name
123 * @param endPtStr The end point name
124 */
125 private static extractNodeName(endPtStr: string): string {
126 const slash: number = endPtStr.indexOf('/');
127 if (slash === -1) {
128 return endPtStr;
129 } else {
130 const afterSlash = endPtStr.substr(slash + 1);
131 if (afterSlash === 'None') {
132 return endPtStr;
133 } else {
134 return endPtStr.substr(0, slash);
135 }
136 }
137 }
138
Sean Condonee545762019-03-09 10:43:58 +0000139 /**
140 * Recursive method to compare 2 objects attribute by attribute and update
141 * the first where a change is detected
142 * @param existingNode 1st object
143 * @param updatedNode 2nd object
144 */
Sean Condon28884332019-03-21 14:07:00 +0000145 private static updateObject(existingNode: Object, updatedNode: Object): ChangeSummary {
146 const changed = <ChangeSummary>{numChanges: 0, locationChanged: false};
Sean Condonee545762019-03-09 10:43:58 +0000147 for (const key of Object.keys(updatedNode)) {
148 const o = updatedNode[key];
Sean Condon28884332019-03-21 14:07:00 +0000149 if (['id', 'x', 'y', 'fx', 'fy', 'vx', 'vy', 'index'].some(k => k === key)) {
Sean Condonee545762019-03-09 10:43:58 +0000150 continue;
151 } else if (o && typeof o === 'object' && o.constructor === Object) {
Sean Condon28884332019-03-21 14:07:00 +0000152 const subChanged = ForceSvgComponent.updateObject(existingNode[key], updatedNode[key]);
153 changed.numChanges += subChanged.numChanges;
154 changed.locationChanged = subChanged.locationChanged ? true : changed.locationChanged;
155 } else if (existingNode === undefined) {
156 // Copy the whole object
157 existingNode = updatedNode;
158 changed.locationChanged = true;
159 changed.numChanges++;
Sean Condonee545762019-03-09 10:43:58 +0000160 } else if (existingNode[key] !== updatedNode[key]) {
Sean Condon28884332019-03-21 14:07:00 +0000161 if (['locType', 'latOrY', 'longOrX', 'latitude', 'longitude', 'gridX', 'gridY'].some(k => k === key)) {
162 changed.locationChanged = true;
163 }
164 changed.numChanges++;
Sean Condonee545762019-03-09 10:43:58 +0000165 existingNode[key] = updatedNode[key];
166 }
167 }
168 return changed;
169 }
170
Sean Condon021f0fa2018-12-06 23:31:11 -0800171 @HostListener('window:resize', ['$event'])
172 onResize(event) {
Sean Condon28884332019-03-21 14:07:00 +0000173 this.graph.restartSimulation();
174 this.log.debug('Simulation restart after resize', event);
Sean Condon021f0fa2018-12-06 23:31:11 -0800175 }
176
177 /**
Sean Condon0c577f62018-11-18 22:40:05 +0000178 * After the component is initialized create the Force simulation
Sean Condon021f0fa2018-12-06 23:31:11 -0800179 * The list of devices, hosts and links will not have been receieved back
180 * from the WebSocket yet as this time - they will be updated later through
181 * ngOnChanges()
Sean Condon0c577f62018-11-18 22:40:05 +0000182 */
183 ngOnInit() {
184 // Receiving an initialized simulated graph from our custom d3 service
Sean Condon28884332019-03-21 14:07:00 +0000185 this.graph = new ForceDirectedGraph(SVGCANVAS, this.log);
Sean Condon0c577f62018-11-18 22:40:05 +0000186
187 /** Binding change detection check on each tick
Sean Condon021f0fa2018-12-06 23:31:11 -0800188 * This along with an onPush change detection strategy should enforce
189 * checking only when relevant! This improves scripting computation
190 * duration in a couple of tests I've made, consistently. Also, it makes
191 * sense to avoid unnecessary checks when we are dealing only with
192 * simulations data binding.
Sean Condon0c577f62018-11-18 22:40:05 +0000193 */
194 this.graph.ticker.subscribe((simulation) => {
Sean Condon28884332019-03-21 14:07:00 +0000195 // this.log.debug("Force simulation has ticked. Alpha",
196 // Math.round(simulation.alpha() * 1000) / 1000);
Sean Condon0c577f62018-11-18 22:40:05 +0000197 this.ref.markForCheck();
198 });
Sean Condon28884332019-03-21 14:07:00 +0000199
Sean Condon0c577f62018-11-18 22:40:05 +0000200 this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
201
Sean Condon0c577f62018-11-18 22:40:05 +0000202 }
203
204 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800205 * When any one of the inputs get changed by a containing component, this
206 * gets called automatically. In addition this is called manually by
207 * topology.service when a response is received from the WebSocket from the
208 * server
Sean Condon0c577f62018-11-18 22:40:05 +0000209 *
210 * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
211 * The Links are added to the Link list of the simulation.
212 * Before they are added the Links are associated with Nodes based on their endPt
213 *
214 * @param changes - a list of changed @Input(s)
215 */
216 ngOnChanges(changes: SimpleChanges) {
217 if (changes['regionData']) {
218 const devices: Device[] =
219 changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
220 const hosts: Host[] =
221 changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
222 const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
223 this.graph.nodes = [];
224 if (devices) {
225 this.graph.nodes = devices;
226 }
227 if (hosts) {
228 this.graph.nodes = this.graph.nodes.concat(hosts);
229 }
230 if (subRegions) {
231 this.graph.nodes = this.graph.nodes.concat(subRegions);
232 }
233
Sean Condon28884332019-03-21 14:07:00 +0000234 this.graph.nodes.forEach((n) => this.fixPosition(n));
Sean Condon71910542019-02-16 18:16:42 +0000235
Sean Condon0c577f62018-11-18 22:40:05 +0000236 // Associate the endpoints of each link with a real node
237 this.graph.links = [];
238 for (const linkIdx of Object.keys(this.regionData.links)) {
Sean Condon021f0fa2018-12-06 23:31:11 -0800239 const epA = ForceSvgComponent.extractNodeName(
240 this.regionData.links[linkIdx].epA);
Sean Condon0c577f62018-11-18 22:40:05 +0000241 this.regionData.links[linkIdx].source =
242 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800243 node.id === epA);
244 const epB = ForceSvgComponent.extractNodeName(
245 this.regionData.links[linkIdx].epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000246 this.regionData.links[linkIdx].target =
247 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800248 node.id === epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000249 this.regionData.links[linkIdx].index = Number(linkIdx);
250 }
251
252 this.graph.links = this.regionData.links;
Sean Condon28884332019-03-21 14:07:00 +0000253 if (this.graph.nodes.length > 0) {
254 this.graph.reinitSimulation();
255 }
Sean Condon0c577f62018-11-18 22:40:05 +0000256 this.log.debug('ForceSvgComponent input changed',
257 this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
258 }
Sean Condon28884332019-03-21 14:07:00 +0000259 }
Sean Condon021f0fa2018-12-06 23:31:11 -0800260
Sean Condon28884332019-03-21 14:07:00 +0000261 /**
262 * If a node has a fixed location then assign it to fx and fy so
263 * that it doesn't get affected by forces
264 * @param graphNode The node whose location should be processed
265 */
266 private fixPosition(graphNode: Node): void {
267 const loc: Location = <Location>graphNode['location'];
268 const props: DeviceProps = <DeviceProps>graphNode['props'];
269 const metaUi = <MetaUi>graphNode['metaUi'];
270 if (loc && loc.locType === LocationType.GEO) {
271 const position: MetaUi =
272 ZoomUtils.convertGeoToCanvas(
273 <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
274 graphNode.fx = position.x;
275 graphNode.fy = position.y;
276 this.log.debug('Found node', graphNode.id, 'with', loc.locType);
277 } else if (loc && loc.locType === LocationType.GRID) {
278 graphNode.fx = loc.longOrX;
279 graphNode.fy = loc.latOrY;
280 this.log.debug('Found node', graphNode.id, 'with', loc.locType);
281 } else if (props && props.locType === LocationType.NONE && metaUi) {
282 graphNode.fx = metaUi.x;
283 graphNode.fy = metaUi.y;
284 this.log.debug('Found node', graphNode.id, 'with locType=none and metaUi');
285 } else {
286 graphNode.fx = null;
287 graphNode.fy = null;
288 }
Sean Condon0c577f62018-11-18 22:40:05 +0000289 }
290
291 /**
292 * Get the index of LayerType so it can drive the visibility of nodes and
293 * hosts on layers
294 */
295 visibleLayerIdx(): number {
296 const layerKeys: string[] = Object.keys(LayerType);
297 for (const idx in layerKeys) {
298 if (LayerType[layerKeys[idx]] === this.visibleLayer) {
299 return Number(idx);
300 }
301 }
302 return -1;
303 }
304
305 selectLink(link: RegionLink): void {
306 this.selectedLink = link;
307 this.linkSelected.emit(link);
308 }
309
Sean Condon021f0fa2018-12-06 23:31:11 -0800310 /**
311 * Iterate through all hosts and devices to deselect the previously selected
312 * node. The emit an event to the parent that lets it know the selection has
313 * changed.
314 * @param selectedNode the newly selected node
315 */
Sean Condon50855cf2018-12-23 15:37:42 +0000316 updateSelected(selectedNode: UiElement): void {
Sean Condon91481822019-01-01 13:56:14 +0000317 this.log.debug('Node or link selected', selectedNode ? selectedNode.id : 'none');
Sean Condon021f0fa2018-12-06 23:31:11 -0800318 this.devices
319 .filter((d) =>
320 selectedNode === undefined || d.device.id !== selectedNode.id)
321 .forEach((d) => d.deselect());
322 this.hosts
323 .filter((h) =>
324 selectedNode === undefined || h.host.id !== selectedNode.id)
325 .forEach((h) => h.deselect());
326
Sean Condon50855cf2018-12-23 15:37:42 +0000327 this.links
328 .filter((l) =>
329 selectedNode === undefined || l.link.id !== selectedNode.id)
330 .forEach((l) => l.deselect());
Sean Condon91481822019-01-01 13:56:14 +0000331 // Push the changes back up to parent (Topology Component)
Sean Condon021f0fa2018-12-06 23:31:11 -0800332 this.selectedNodeEvent.emit(selectedNode);
Sean Condon0c577f62018-11-18 22:40:05 +0000333 }
334
Sean Condon021f0fa2018-12-06 23:31:11 -0800335 /**
336 * We want to filter links to show only those not related to hosts if the
Sean Condon50855cf2018-12-23 15:37:42 +0000337 * 'showHosts' flag has been switched off. If 'showHosts' is true, then
Sean Condon021f0fa2018-12-06 23:31:11 -0800338 * display all links.
339 */
Sean Condon50855cf2018-12-23 15:37:42 +0000340 filteredLinks(): Link[] {
Sean Condon021f0fa2018-12-06 23:31:11 -0800341 return this.regionData.links.filter((h) =>
342 this.showHosts ||
343 ((<Host>h.source).nodeType !== 'host' &&
344 (<Host>h.target).nodeType !== 'host'));
Sean Condon0c577f62018-11-18 22:40:05 +0000345 }
Sean Condon50855cf2018-12-23 15:37:42 +0000346
347 /**
348 * When changes happen in the model, then model events are sent up through the
349 * Web Socket
350 * @param type - the type of the change
351 * @param memo - a qualifier on the type
352 * @param subject - the item that the update is for
353 * @param data - the new definition of the item
354 */
355 handleModelEvent(type: ModelEventType, memo: ModelEventMemo, subject: string, data: UiElement): void {
356 switch (type) {
357 case ModelEventType.DEVICE_ADDED_OR_UPDATED:
358 if (memo === ModelEventMemo.ADDED) {
Sean Condon28884332019-03-21 14:07:00 +0000359 this.fixPosition(<Device>data);
Sean Condonee545762019-03-09 10:43:58 +0000360 this.graph.nodes.push(<Device>data);
Sean Condonee5d4b92019-03-11 19:57:34 +0000361 this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
Sean Condonee545762019-03-09 10:43:58 +0000362 this.log.debug('Device added', (<Device>data).id);
Sean Condon50855cf2018-12-23 15:37:42 +0000363 } else if (memo === ModelEventMemo.UPDATED) {
364 const oldDevice: Device =
365 this.regionData.devices[this.visibleLayerIdx()]
366 .find((d) => d.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000367 const changes = ForceSvgComponent.updateObject(oldDevice, <Device>data);
Sean Condon28884332019-03-21 14:07:00 +0000368 if (changes.numChanges > 0) {
Sean Condonee545762019-03-09 10:43:58 +0000369 this.log.debug('Device ', oldDevice.id, memo, ' - ', changes, 'changes');
Sean Condon28884332019-03-21 14:07:00 +0000370 if (changes.locationChanged) {
371 this.fixPosition(oldDevice);
372 }
Sean Condonee545762019-03-09 10:43:58 +0000373 }
Sean Condon50855cf2018-12-23 15:37:42 +0000374 } else {
375 this.log.warn('Device ', memo, ' - not yet implemented', data);
376 }
Sean Condon50855cf2018-12-23 15:37:42 +0000377 break;
378 case ModelEventType.HOST_ADDED_OR_UPDATED:
379 if (memo === ModelEventMemo.ADDED) {
Sean Condon28884332019-03-21 14:07:00 +0000380 this.fixPosition(<Host>data);
Sean Condonee545762019-03-09 10:43:58 +0000381 this.graph.nodes.push(<Host>data);
Sean Condon28884332019-03-21 14:07:00 +0000382 this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
Sean Condonee545762019-03-09 10:43:58 +0000383 this.log.debug('Host added', (<Host>data).id);
Sean Condon50855cf2018-12-23 15:37:42 +0000384 } else if (memo === ModelEventMemo.UPDATED) {
385 const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
386 .find((h) => h.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000387 const changes = ForceSvgComponent.updateObject(oldHost, <Host>data);
Sean Condon28884332019-03-21 14:07:00 +0000388 if (changes.numChanges > 0) {
Sean Condonee545762019-03-09 10:43:58 +0000389 this.log.debug('Host ', oldHost.id, memo, ' - ', changes, 'changes');
Sean Condon28884332019-03-21 14:07:00 +0000390 if (changes.locationChanged) {
391 this.fixPosition(oldHost);
392 }
Sean Condonee545762019-03-09 10:43:58 +0000393 }
Sean Condon50855cf2018-12-23 15:37:42 +0000394 } else {
395 this.log.warn('Host change', memo, ' - unexpected');
396 }
397 break;
398 case ModelEventType.DEVICE_REMOVED:
399 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
400 const removeIdx: number =
401 this.regionData.devices[this.visibleLayerIdx()]
402 .findIndex((d) => d.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000403 this.regionData.devices[this.visibleLayerIdx()].splice(removeIdx, 1);
404 this.removeRelatedLinks(subject);
405 this.log.debug('Device ', subject, 'removed. Links', this.regionData.links);
Sean Condon50855cf2018-12-23 15:37:42 +0000406 } else {
407 this.log.warn('Device removed - unexpected memo', memo);
408 }
409 break;
410 case ModelEventType.HOST_REMOVED:
411 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
412 const removeIdx: number =
413 this.regionData.hosts[this.visibleLayerIdx()]
414 .findIndex((h) => h.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000415 this.regionData.hosts[this.visibleLayerIdx()].splice(removeIdx, 1);
416 this.removeRelatedLinks(subject);
417 this.log.warn('Host ', subject, 'removed');
Sean Condon50855cf2018-12-23 15:37:42 +0000418 } else {
419 this.log.warn('Host removed - unexpected memo', memo);
420 }
421 break;
422 case ModelEventType.LINK_ADDED_OR_UPDATED:
Sean Condonee545762019-03-09 10:43:58 +0000423 if (memo === ModelEventMemo.ADDED &&
424 this.regionData.links.findIndex((l) => l.id === subject) === -1) {
425 const listLen = this.regionData.links.push(<RegionLink>data);
426 const epA = ForceSvgComponent.extractNodeName(
427 this.regionData.links[listLen - 1].epA);
428 this.regionData.links[listLen - 1].source =
429 this.graph.nodes.find((node) =>
430 node.id === epA);
431 const epB = ForceSvgComponent.extractNodeName(
432 this.regionData.links[listLen - 1].epB);
433 this.regionData.links[listLen - 1].target =
434 this.graph.nodes.find((node) =>
435 node.id === epB);
436 this.log.debug('Link added', subject);
437 } else if (memo === ModelEventMemo.UPDATED) {
438 const oldLink = this.regionData.links.find((l) => l.id === subject);
439 const changes = ForceSvgComponent.updateObject(oldLink, <RegionLink>data);
440 this.log.debug('Link ', subject, '. Updated', changes, 'items');
441 } else {
442 this.log.warn('Link added or updated - unexpected memo', memo);
443 }
Sean Condon50855cf2018-12-23 15:37:42 +0000444 break;
445 default:
446 this.log.error('Unexpected model event', type, 'for', subject);
447 }
Sean Condon28884332019-03-21 14:07:00 +0000448 this.graph.links = this.regionData.links;
449 this.graph.reinitSimulation();
Sean Condon50855cf2018-12-23 15:37:42 +0000450 }
451
Sean Condonee545762019-03-09 10:43:58 +0000452 private removeRelatedLinks(subject: string) {
453 const len = this.regionData.links.length;
454 for (let i = 0; i < len; i++) {
455 const linkIdx = this.regionData.links.findIndex((l) =>
456 (ForceSvgComponent.extractNodeName(l.epA) === subject ||
457 ForceSvgComponent.extractNodeName(l.epB) === subject));
458 if (linkIdx >= 0) {
459 this.regionData.links.splice(linkIdx, 1);
460 this.log.debug('Link ', linkIdx, 'removed on attempt', i);
461 }
Sean Condon50855cf2018-12-23 15:37:42 +0000462 }
463 }
464
465 /**
466 * When traffic monitoring is turned on (A key) highlights will be sent back
467 * from the WebSocket through the Traffic Service
468 * @param devices - an array of device highlights
469 * @param hosts - an array of host highlights
470 * @param links - an array of link highlights
471 */
472 handleHighlights(devices: Device[], hosts: Host[], links: LinkHighlight[]): void {
473
474 if (devices.length > 0) {
475 this.log.debug(devices.length, 'Devices highlighted');
476 devices.forEach((dh) => {
477 const deviceComponent: DeviceNodeSvgComponent = this.devices.find((d) => d.device.id === dh.id );
478 if (deviceComponent) {
479 deviceComponent.ngOnChanges(
480 {'deviceHighlight': new SimpleChange(<Device>{}, dh, true)}
481 );
482 this.log.debug('Highlighting device', deviceComponent.device.id);
483 } else {
484 this.log.warn('Device component not found', dh.id);
485 }
486 });
487 }
488 if (hosts.length > 0) {
489 this.log.debug(hosts.length, 'Hosts highlighted');
490 hosts.forEach((hh) => {
491 const hostComponent: HostNodeSvgComponent = this.hosts.find((h) => h.host.id === hh.id );
492 if (hostComponent) {
493 hostComponent.ngOnChanges(
494 {'hostHighlight': new SimpleChange(<Host>{}, hh, true)}
495 );
496 this.log.debug('Highlighting host', hostComponent.host.id);
497 }
498 });
499 }
500 if (links.length > 0) {
501 this.log.debug(links.length, 'Links highlighted');
502 links.forEach((lh) => {
503 const linkComponent: LinkSvgComponent = this.links.find((l) => l.link.id === lh.id );
504 if (linkComponent) { // A link might not be present is hosts viewing is switched off
505 linkComponent.ngOnChanges(
506 {'linkHighlight': new SimpleChange(<LinkHighlight>{}, lh, true)}
507 );
508 // this.log.debug('Highlighting link', linkComponent.link.id, lh.css, lh.label);
509 }
510 });
511 }
512 }
Sean Condon71910542019-02-16 18:16:42 +0000513
514 /**
515 * As nodes are dragged around the graph, their new location should be sent
516 * back to server
517 * @param klass The class of node e.g. 'host' or 'device'
518 * @param id - the ID of the node
519 * @param newLocation - the new Location of the node
520 */
521 nodeMoved(klass: string, id: string, newLocation: MetaUi) {
Sean Condon28884332019-03-21 14:07:00 +0000522 this.wss.sendEvent('updateMeta2', <UpdateMeta>{
Sean Condon71910542019-02-16 18:16:42 +0000523 id: id,
524 class: klass,
525 memento: newLocation
526 });
527 this.log.debug(klass, id, 'has been moved to', newLocation);
528 }
Sean Condon1ae15802019-03-02 09:07:18 +0000529
530 resetNodeLocations() {
531 this.devices.forEach((d) => {
532 d.resetNodeLocation();
533 });
534 }
Sean Condonf4f54a12018-10-10 23:25:46 +0100535}
Sean Condon0c577f62018-11-18 22:40:05 +0000536