blob: 69103534711698915de99dfca117bcc53772d5e8 [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 Condonee545762019-03-09 10:43:58 +000055import {LocationType} from '../backgroundsvg/backgroundsvg.component';
Sean Condonff85fbe2019-03-16 14:28:46 +000056import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
57import { HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
58import { LinkSvgComponent} from './visuals/linksvg/linksvg.component';
Sean Condon71910542019-02-16 18:16:42 +000059
60interface UpdateMeta {
61 id: string;
62 class: string;
63 memento: MetaUi;
64}
Sean Condonaa4366d2018-11-02 14:29:01 +000065
Sean Condonf4f54a12018-10-10 23:25:46 +010066/**
67 * ONOS GUI -- Topology Forces Graph Layer View.
Sean Condon0c577f62018-11-18 22:40:05 +000068 *
69 * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
70 * This drives the whole Force graph
Sean Condonf4f54a12018-10-10 23:25:46 +010071 */
72@Component({
73 selector: '[onos-forcesvg]',
74 templateUrl: './forcesvg.component.html',
Sean Condon0c577f62018-11-18 22:40:05 +000075 styleUrls: ['./forcesvg.component.css'],
76 changeDetection: ChangeDetectionStrategy.OnPush,
Sean Condonf4f54a12018-10-10 23:25:46 +010077})
Sean Condon0c577f62018-11-18 22:40:05 +000078export class ForceSvgComponent implements OnInit, OnChanges {
Sean Condonff85fbe2019-03-16 14:28:46 +000079 @Input() deviceLabelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
80 @Input() hostLabelToggle: HostLabelToggle.Enum = HostLabelToggle.Enum.NONE;
Sean Condonb2c483c2019-01-16 20:28:55 +000081 @Input() showHosts: boolean = false;
82 @Input() highlightPorts: boolean = true;
83 @Input() onosInstMastership: string = '';
84 @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
85 @Input() selectedLink: RegionLink = null;
Sean Condon1ae15802019-03-02 09:07:18 +000086 @Input() scale: number = 1;
Sean Condon0c577f62018-11-18 22:40:05 +000087 @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
Sean Condonb2c483c2019-01-16 20:28:55 +000088 @Output() linkSelected = new EventEmitter<RegionLink>();
89 @Output() selectedNodeEvent = new EventEmitter<UiElement>();
Sean Condonff85fbe2019-03-16 14:28:46 +000090 public graph: ForceDirectedGraph;
Sean Condon0c577f62018-11-18 22:40:05 +000091 private _options: { width, height } = { width: 800, height: 600 };
Sean Condonf4f54a12018-10-10 23:25:46 +010092
Sean Condon021f0fa2018-12-06 23:31:11 -080093 // References to the children of this component - these are created in the
94 // template view with the *ngFor and we get them by a query here
Sean Condon0c577f62018-11-18 22:40:05 +000095 @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
Sean Condon021f0fa2018-12-06 23:31:11 -080096 @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
Sean Condon50855cf2018-12-23 15:37:42 +000097 @ViewChildren(LinkSvgComponent) links: QueryList<LinkSvgComponent>;
Sean Condonf4f54a12018-10-10 23:25:46 +010098
Sean Condon0c577f62018-11-18 22:40:05 +000099 constructor(
100 protected log: LogService,
Sean Condon71910542019-02-16 18:16:42 +0000101 private ref: ChangeDetectorRef,
102 protected wss: WebSocketService
Sean Condon0c577f62018-11-18 22:40:05 +0000103 ) {
104 this.selectedLink = null;
105 this.log.debug('ForceSvgComponent constructed');
106 }
107
108 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800109 * Utility for extracting a node name from an endpoint string
110 * In some cases - have to remove the port number from the end of a device
111 * name
112 * @param endPtStr The end point name
113 */
114 private static extractNodeName(endPtStr: string): string {
115 const slash: number = endPtStr.indexOf('/');
116 if (slash === -1) {
117 return endPtStr;
118 } else {
119 const afterSlash = endPtStr.substr(slash + 1);
120 if (afterSlash === 'None') {
121 return endPtStr;
122 } else {
123 return endPtStr.substr(0, slash);
124 }
125 }
126 }
127
Sean Condonee545762019-03-09 10:43:58 +0000128 /**
129 * Recursive method to compare 2 objects attribute by attribute and update
130 * the first where a change is detected
131 * @param existingNode 1st object
132 * @param updatedNode 2nd object
133 */
134 private static updateObject(existingNode: Object, updatedNode: Object): number {
135 let changed: number = 0;
136 for (const key of Object.keys(updatedNode)) {
137 const o = updatedNode[key];
138 if (key === 'id') {
139 continue;
140 } else if (o && typeof o === 'object' && o.constructor === Object) {
141 changed += ForceSvgComponent.updateObject(existingNode[key], updatedNode[key]);
142 } else if (existingNode[key] !== updatedNode[key]) {
143 changed++;
144 existingNode[key] = updatedNode[key];
145 }
146 }
147 return changed;
148 }
149
Sean Condon021f0fa2018-12-06 23:31:11 -0800150 @HostListener('window:resize', ['$event'])
151 onResize(event) {
152 this.graph.initSimulation(this.options);
153 this.log.debug('Simulation reinit after resize', event);
154 }
155
156 /**
Sean Condon0c577f62018-11-18 22:40:05 +0000157 * After the component is initialized create the Force simulation
Sean Condon021f0fa2018-12-06 23:31:11 -0800158 * The list of devices, hosts and links will not have been receieved back
159 * from the WebSocket yet as this time - they will be updated later through
160 * ngOnChanges()
Sean Condon0c577f62018-11-18 22:40:05 +0000161 */
162 ngOnInit() {
163 // Receiving an initialized simulated graph from our custom d3 service
Sean Condon50855cf2018-12-23 15:37:42 +0000164 this.graph = new ForceDirectedGraph(this.options, this.log);
Sean Condon0c577f62018-11-18 22:40:05 +0000165
166 /** Binding change detection check on each tick
Sean Condon021f0fa2018-12-06 23:31:11 -0800167 * This along with an onPush change detection strategy should enforce
168 * checking only when relevant! This improves scripting computation
169 * duration in a couple of tests I've made, consistently. Also, it makes
170 * sense to avoid unnecessary checks when we are dealing only with
171 * simulations data binding.
Sean Condon0c577f62018-11-18 22:40:05 +0000172 */
173 this.graph.ticker.subscribe((simulation) => {
174 // this.log.debug("Force simulation has ticked", simulation);
175 this.ref.markForCheck();
176 });
177 this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
178
Sean Condon0c577f62018-11-18 22:40:05 +0000179 }
180
181 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800182 * When any one of the inputs get changed by a containing component, this
183 * gets called automatically. In addition this is called manually by
184 * topology.service when a response is received from the WebSocket from the
185 * server
Sean Condon0c577f62018-11-18 22:40:05 +0000186 *
187 * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
188 * The Links are added to the Link list of the simulation.
189 * Before they are added the Links are associated with Nodes based on their endPt
190 *
191 * @param changes - a list of changed @Input(s)
192 */
193 ngOnChanges(changes: SimpleChanges) {
194 if (changes['regionData']) {
195 const devices: Device[] =
196 changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
197 const hosts: Host[] =
198 changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
199 const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
200 this.graph.nodes = [];
201 if (devices) {
202 this.graph.nodes = devices;
203 }
204 if (hosts) {
205 this.graph.nodes = this.graph.nodes.concat(hosts);
206 }
207 if (subRegions) {
208 this.graph.nodes = this.graph.nodes.concat(subRegions);
209 }
210
Sean Condon71910542019-02-16 18:16:42 +0000211 // If a node has a fixed location then assign it to fx and fy so
212 // that it doesn't get affected by forces
213 this.graph.nodes
214 .forEach((n) => {
215 const loc: Location = <Location>n['location'];
216 if (loc && loc.locType === LocationType.GEO) {
217 const position: MetaUi =
Sean Condon1ae15802019-03-02 09:07:18 +0000218 ZoomUtils.convertGeoToCanvas(
Sean Condon71910542019-02-16 18:16:42 +0000219 <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
220 n.fx = position.x;
221 n.fy = position.y;
222 this.log.debug('Found node', n.id, 'with', loc.locType);
223 }
224 });
225
Sean Condon0c577f62018-11-18 22:40:05 +0000226 // Associate the endpoints of each link with a real node
227 this.graph.links = [];
228 for (const linkIdx of Object.keys(this.regionData.links)) {
Sean Condon021f0fa2018-12-06 23:31:11 -0800229 const epA = ForceSvgComponent.extractNodeName(
230 this.regionData.links[linkIdx].epA);
Sean Condon0c577f62018-11-18 22:40:05 +0000231 this.regionData.links[linkIdx].source =
232 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800233 node.id === epA);
234 const epB = ForceSvgComponent.extractNodeName(
235 this.regionData.links[linkIdx].epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000236 this.regionData.links[linkIdx].target =
237 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800238 node.id === epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000239 this.regionData.links[linkIdx].index = Number(linkIdx);
240 }
241
242 this.graph.links = this.regionData.links;
243
244 this.graph.initSimulation(this.options);
245 this.graph.initNodes();
Sean Condon50855cf2018-12-23 15:37:42 +0000246 this.graph.initLinks();
Sean Condon0c577f62018-11-18 22:40:05 +0000247 this.log.debug('ForceSvgComponent input changed',
248 this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
249 }
Sean Condon021f0fa2018-12-06 23:31:11 -0800250
Sean Condon021f0fa2018-12-06 23:31:11 -0800251 this.ref.markForCheck();
Sean Condon0c577f62018-11-18 22:40:05 +0000252 }
253
254 /**
255 * Get the index of LayerType so it can drive the visibility of nodes and
256 * hosts on layers
257 */
258 visibleLayerIdx(): number {
259 const layerKeys: string[] = Object.keys(LayerType);
260 for (const idx in layerKeys) {
261 if (LayerType[layerKeys[idx]] === this.visibleLayer) {
262 return Number(idx);
263 }
264 }
265 return -1;
266 }
267
268 selectLink(link: RegionLink): void {
269 this.selectedLink = link;
270 this.linkSelected.emit(link);
271 }
272
273 get options() {
274 return this._options = {
275 width: window.innerWidth,
276 height: window.innerHeight
277 };
278 }
279
Sean Condon021f0fa2018-12-06 23:31:11 -0800280 /**
281 * Iterate through all hosts and devices to deselect the previously selected
282 * node. The emit an event to the parent that lets it know the selection has
283 * changed.
284 * @param selectedNode the newly selected node
285 */
Sean Condon50855cf2018-12-23 15:37:42 +0000286 updateSelected(selectedNode: UiElement): void {
Sean Condon91481822019-01-01 13:56:14 +0000287 this.log.debug('Node or link selected', selectedNode ? selectedNode.id : 'none');
Sean Condon021f0fa2018-12-06 23:31:11 -0800288 this.devices
289 .filter((d) =>
290 selectedNode === undefined || d.device.id !== selectedNode.id)
291 .forEach((d) => d.deselect());
292 this.hosts
293 .filter((h) =>
294 selectedNode === undefined || h.host.id !== selectedNode.id)
295 .forEach((h) => h.deselect());
296
Sean Condon50855cf2018-12-23 15:37:42 +0000297 this.links
298 .filter((l) =>
299 selectedNode === undefined || l.link.id !== selectedNode.id)
300 .forEach((l) => l.deselect());
Sean Condon91481822019-01-01 13:56:14 +0000301 // Push the changes back up to parent (Topology Component)
Sean Condon021f0fa2018-12-06 23:31:11 -0800302 this.selectedNodeEvent.emit(selectedNode);
Sean Condon0c577f62018-11-18 22:40:05 +0000303 }
304
Sean Condon021f0fa2018-12-06 23:31:11 -0800305 /**
306 * We want to filter links to show only those not related to hosts if the
Sean Condon50855cf2018-12-23 15:37:42 +0000307 * 'showHosts' flag has been switched off. If 'showHosts' is true, then
Sean Condon021f0fa2018-12-06 23:31:11 -0800308 * display all links.
309 */
Sean Condon50855cf2018-12-23 15:37:42 +0000310 filteredLinks(): Link[] {
Sean Condon021f0fa2018-12-06 23:31:11 -0800311 return this.regionData.links.filter((h) =>
312 this.showHosts ||
313 ((<Host>h.source).nodeType !== 'host' &&
314 (<Host>h.target).nodeType !== 'host'));
Sean Condon0c577f62018-11-18 22:40:05 +0000315 }
Sean Condon50855cf2018-12-23 15:37:42 +0000316
317 /**
318 * When changes happen in the model, then model events are sent up through the
319 * Web Socket
320 * @param type - the type of the change
321 * @param memo - a qualifier on the type
322 * @param subject - the item that the update is for
323 * @param data - the new definition of the item
324 */
325 handleModelEvent(type: ModelEventType, memo: ModelEventMemo, subject: string, data: UiElement): void {
326 switch (type) {
327 case ModelEventType.DEVICE_ADDED_OR_UPDATED:
328 if (memo === ModelEventMemo.ADDED) {
Sean Condonee5d4b92019-03-11 19:57:34 +0000329 const loc = (<Device>data).location;
330 if (loc && loc.locType === LocationType.GEO) {
331 const position =
332 ZoomUtils.convertGeoToCanvas(<LocMeta>{ lng: loc.longOrX, lat: loc.latOrY});
333 (<Device>data).fx = position.x;
334 (<Device>data).fy = position.y;
335 this.log.debug('Using long', loc.longOrX, 'lat', loc.latOrY, '(', position.x, position.y, ')');
336 } else if (loc && loc.locType === LocationType.GRID) {
337 (<Device>data).fx = loc.longOrX;
338 (<Device>data).fy = loc.latOrY;
339 this.log.debug('Using grid', loc.longOrX, loc.latOrY);
340 } else {
341 (<Device>data).fx = null;
342 (<Device>data).fy = null;
343 // (<Device>data).x = 500;
344 // (<Device>data).y = 500;
345 }
Sean Condonee545762019-03-09 10:43:58 +0000346 this.graph.nodes.push(<Device>data);
Sean Condonee5d4b92019-03-11 19:57:34 +0000347 this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
Sean Condonee545762019-03-09 10:43:58 +0000348 this.log.debug('Device added', (<Device>data).id);
Sean Condon50855cf2018-12-23 15:37:42 +0000349 } else if (memo === ModelEventMemo.UPDATED) {
350 const oldDevice: Device =
351 this.regionData.devices[this.visibleLayerIdx()]
352 .find((d) => d.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000353 const changes = ForceSvgComponent.updateObject(oldDevice, <Device>data);
354 if (changes > 0) {
355 this.log.debug('Device ', oldDevice.id, memo, ' - ', changes, 'changes');
356 }
Sean Condon50855cf2018-12-23 15:37:42 +0000357 } else {
358 this.log.warn('Device ', memo, ' - not yet implemented', data);
359 }
Sean Condon50855cf2018-12-23 15:37:42 +0000360 break;
361 case ModelEventType.HOST_ADDED_OR_UPDATED:
362 if (memo === ModelEventMemo.ADDED) {
363 this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
Sean Condonee545762019-03-09 10:43:58 +0000364 this.graph.nodes.push(<Host>data);
365 this.log.debug('Host added', (<Host>data).id);
Sean Condon50855cf2018-12-23 15:37:42 +0000366 } else if (memo === ModelEventMemo.UPDATED) {
367 const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
368 .find((h) => h.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000369 const changes = ForceSvgComponent.updateObject(oldHost, <Host>data);
370 if (changes > 0) {
371 this.log.debug('Host ', oldHost.id, memo, ' - ', changes, 'changes');
372 }
Sean Condon50855cf2018-12-23 15:37:42 +0000373 } else {
374 this.log.warn('Host change', memo, ' - unexpected');
375 }
376 break;
377 case ModelEventType.DEVICE_REMOVED:
378 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
379 const removeIdx: number =
380 this.regionData.devices[this.visibleLayerIdx()]
381 .findIndex((d) => d.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000382 this.regionData.devices[this.visibleLayerIdx()].splice(removeIdx, 1);
383 this.removeRelatedLinks(subject);
384 this.log.debug('Device ', subject, 'removed. Links', this.regionData.links);
Sean Condon50855cf2018-12-23 15:37:42 +0000385 } else {
386 this.log.warn('Device removed - unexpected memo', memo);
387 }
388 break;
389 case ModelEventType.HOST_REMOVED:
390 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
391 const removeIdx: number =
392 this.regionData.hosts[this.visibleLayerIdx()]
393 .findIndex((h) => h.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000394 this.regionData.hosts[this.visibleLayerIdx()].splice(removeIdx, 1);
395 this.removeRelatedLinks(subject);
396 this.log.warn('Host ', subject, 'removed');
Sean Condon50855cf2018-12-23 15:37:42 +0000397 } else {
398 this.log.warn('Host removed - unexpected memo', memo);
399 }
400 break;
401 case ModelEventType.LINK_ADDED_OR_UPDATED:
Sean Condonee545762019-03-09 10:43:58 +0000402 if (memo === ModelEventMemo.ADDED &&
403 this.regionData.links.findIndex((l) => l.id === subject) === -1) {
404 const listLen = this.regionData.links.push(<RegionLink>data);
405 const epA = ForceSvgComponent.extractNodeName(
406 this.regionData.links[listLen - 1].epA);
407 this.regionData.links[listLen - 1].source =
408 this.graph.nodes.find((node) =>
409 node.id === epA);
410 const epB = ForceSvgComponent.extractNodeName(
411 this.regionData.links[listLen - 1].epB);
412 this.regionData.links[listLen - 1].target =
413 this.graph.nodes.find((node) =>
414 node.id === epB);
415 this.log.debug('Link added', subject);
416 } else if (memo === ModelEventMemo.UPDATED) {
417 const oldLink = this.regionData.links.find((l) => l.id === subject);
418 const changes = ForceSvgComponent.updateObject(oldLink, <RegionLink>data);
419 this.log.debug('Link ', subject, '. Updated', changes, 'items');
420 } else {
421 this.log.warn('Link added or updated - unexpected memo', memo);
422 }
Sean Condon50855cf2018-12-23 15:37:42 +0000423 break;
424 default:
425 this.log.error('Unexpected model event', type, 'for', subject);
426 }
Sean Condonee545762019-03-09 10:43:58 +0000427 this.ref.markForCheck();
428 this.graph.initSimulation(this.options);
Sean Condonee5d4b92019-03-11 19:57:34 +0000429 this.graph.initNodes();
430 this.graph.initLinks();
Sean Condon50855cf2018-12-23 15:37:42 +0000431 }
432
Sean Condonee545762019-03-09 10:43:58 +0000433 private removeRelatedLinks(subject: string) {
434 const len = this.regionData.links.length;
435 for (let i = 0; i < len; i++) {
436 const linkIdx = this.regionData.links.findIndex((l) =>
437 (ForceSvgComponent.extractNodeName(l.epA) === subject ||
438 ForceSvgComponent.extractNodeName(l.epB) === subject));
439 if (linkIdx >= 0) {
440 this.regionData.links.splice(linkIdx, 1);
441 this.log.debug('Link ', linkIdx, 'removed on attempt', i);
442 }
Sean Condon50855cf2018-12-23 15:37:42 +0000443 }
444 }
445
446 /**
447 * When traffic monitoring is turned on (A key) highlights will be sent back
448 * from the WebSocket through the Traffic Service
449 * @param devices - an array of device highlights
450 * @param hosts - an array of host highlights
451 * @param links - an array of link highlights
452 */
453 handleHighlights(devices: Device[], hosts: Host[], links: LinkHighlight[]): void {
454
455 if (devices.length > 0) {
456 this.log.debug(devices.length, 'Devices highlighted');
457 devices.forEach((dh) => {
458 const deviceComponent: DeviceNodeSvgComponent = this.devices.find((d) => d.device.id === dh.id );
459 if (deviceComponent) {
460 deviceComponent.ngOnChanges(
461 {'deviceHighlight': new SimpleChange(<Device>{}, dh, true)}
462 );
463 this.log.debug('Highlighting device', deviceComponent.device.id);
464 } else {
465 this.log.warn('Device component not found', dh.id);
466 }
467 });
468 }
469 if (hosts.length > 0) {
470 this.log.debug(hosts.length, 'Hosts highlighted');
471 hosts.forEach((hh) => {
472 const hostComponent: HostNodeSvgComponent = this.hosts.find((h) => h.host.id === hh.id );
473 if (hostComponent) {
474 hostComponent.ngOnChanges(
475 {'hostHighlight': new SimpleChange(<Host>{}, hh, true)}
476 );
477 this.log.debug('Highlighting host', hostComponent.host.id);
478 }
479 });
480 }
481 if (links.length > 0) {
482 this.log.debug(links.length, 'Links highlighted');
483 links.forEach((lh) => {
484 const linkComponent: LinkSvgComponent = this.links.find((l) => l.link.id === lh.id );
485 if (linkComponent) { // A link might not be present is hosts viewing is switched off
486 linkComponent.ngOnChanges(
487 {'linkHighlight': new SimpleChange(<LinkHighlight>{}, lh, true)}
488 );
489 // this.log.debug('Highlighting link', linkComponent.link.id, lh.css, lh.label);
490 }
491 });
492 }
493 }
Sean Condon71910542019-02-16 18:16:42 +0000494
495 /**
496 * As nodes are dragged around the graph, their new location should be sent
497 * back to server
498 * @param klass The class of node e.g. 'host' or 'device'
499 * @param id - the ID of the node
500 * @param newLocation - the new Location of the node
501 */
502 nodeMoved(klass: string, id: string, newLocation: MetaUi) {
503 this.wss.sendEvent('updateMeta', <UpdateMeta>{
504 id: id,
505 class: klass,
506 memento: newLocation
507 });
508 this.log.debug(klass, id, 'has been moved to', newLocation);
509 }
Sean Condon1ae15802019-03-02 09:07:18 +0000510
511 resetNodeLocations() {
512 this.devices.forEach((d) => {
513 d.resetNodeLocation();
514 });
515 }
Sean Condonf4f54a12018-10-10 23:25:46 +0100516}
Sean Condon0c577f62018-11-18 22:40:05 +0000517