blob: b759997f732f482f57c4d97c3e1a09e38d1f4cd2 [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 Condond88f3662019-04-03 16:35:30 +010051 Options,
Sean Condon0c577f62018-11-18 22:40:05 +000052 Region,
53 RegionLink,
Sean Condon50855cf2018-12-23 15:37:42 +000054 SubRegion,
55 UiElement
Sean Condon0c577f62018-11-18 22:40:05 +000056} from './models';
Sean Condonee545762019-03-09 10:43:58 +000057import {LocationType} from '../backgroundsvg/backgroundsvg.component';
Sean Condonff85fbe2019-03-16 14:28:46 +000058import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
59import { HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
60import { LinkSvgComponent} from './visuals/linksvg/linksvg.component';
Sean Condond88f3662019-04-03 16:35:30 +010061import {SelectedEvent} from './visuals/nodevisual';
Sean Condon71910542019-02-16 18:16:42 +000062
63interface UpdateMeta {
64 id: string;
65 class: string;
66 memento: MetaUi;
67}
Sean Condonaa4366d2018-11-02 14:29:01 +000068
Sean Condon28884332019-03-21 14:07:00 +000069const SVGCANVAS = <Options>{
70 width: 1000,
71 height: 1000
72};
73
74interface ChangeSummary {
75 numChanges: number;
76 locationChanged: boolean;
77}
78
Sean Condonf4f54a12018-10-10 23:25:46 +010079/**
80 * ONOS GUI -- Topology Forces Graph Layer View.
Sean Condon0c577f62018-11-18 22:40:05 +000081 *
82 * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
83 * This drives the whole Force graph
Sean Condonf4f54a12018-10-10 23:25:46 +010084 */
85@Component({
86 selector: '[onos-forcesvg]',
87 templateUrl: './forcesvg.component.html',
Sean Condon0c577f62018-11-18 22:40:05 +000088 styleUrls: ['./forcesvg.component.css'],
89 changeDetection: ChangeDetectionStrategy.OnPush,
Sean Condonf4f54a12018-10-10 23:25:46 +010090})
Sean Condon0c577f62018-11-18 22:40:05 +000091export class ForceSvgComponent implements OnInit, OnChanges {
Sean Condonff85fbe2019-03-16 14:28:46 +000092 @Input() deviceLabelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
93 @Input() hostLabelToggle: HostLabelToggle.Enum = HostLabelToggle.Enum.NONE;
Sean Condonb2c483c2019-01-16 20:28:55 +000094 @Input() showHosts: boolean = false;
95 @Input() highlightPorts: boolean = true;
96 @Input() onosInstMastership: string = '';
97 @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
98 @Input() selectedLink: RegionLink = null;
Sean Condon1ae15802019-03-02 09:07:18 +000099 @Input() scale: number = 1;
Sean Condon0c577f62018-11-18 22:40:05 +0000100 @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
Sean Condonb2c483c2019-01-16 20:28:55 +0000101 @Output() linkSelected = new EventEmitter<RegionLink>();
Sean Condond88f3662019-04-03 16:35:30 +0100102 @Output() selectedNodeEvent = new EventEmitter<UiElement[]>();
Sean Condonff85fbe2019-03-16 14:28:46 +0000103 public graph: ForceDirectedGraph;
Sean Condond88f3662019-04-03 16:35:30 +0100104 private selectedNodes: UiElement[] = [];
Sean Condonf4f54a12018-10-10 23:25:46 +0100105
Sean Condon021f0fa2018-12-06 23:31:11 -0800106 // References to the children of this component - these are created in the
107 // template view with the *ngFor and we get them by a query here
Sean Condon0c577f62018-11-18 22:40:05 +0000108 @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
Sean Condon021f0fa2018-12-06 23:31:11 -0800109 @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
Sean Condon50855cf2018-12-23 15:37:42 +0000110 @ViewChildren(LinkSvgComponent) links: QueryList<LinkSvgComponent>;
Sean Condonf4f54a12018-10-10 23:25:46 +0100111
Sean Condon0c577f62018-11-18 22:40:05 +0000112 constructor(
113 protected log: LogService,
Sean Condon71910542019-02-16 18:16:42 +0000114 private ref: ChangeDetectorRef,
115 protected wss: WebSocketService
Sean Condon0c577f62018-11-18 22:40:05 +0000116 ) {
117 this.selectedLink = null;
118 this.log.debug('ForceSvgComponent constructed');
119 }
120
121 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800122 * Utility for extracting a node name from an endpoint string
123 * In some cases - have to remove the port number from the end of a device
124 * name
125 * @param endPtStr The end point name
126 */
127 private static extractNodeName(endPtStr: string): string {
128 const slash: number = endPtStr.indexOf('/');
129 if (slash === -1) {
130 return endPtStr;
131 } else {
132 const afterSlash = endPtStr.substr(slash + 1);
133 if (afterSlash === 'None') {
134 return endPtStr;
135 } else {
136 return endPtStr.substr(0, slash);
137 }
138 }
139 }
140
Sean Condonee545762019-03-09 10:43:58 +0000141 /**
142 * Recursive method to compare 2 objects attribute by attribute and update
143 * the first where a change is detected
144 * @param existingNode 1st object
145 * @param updatedNode 2nd object
146 */
Sean Condon28884332019-03-21 14:07:00 +0000147 private static updateObject(existingNode: Object, updatedNode: Object): ChangeSummary {
148 const changed = <ChangeSummary>{numChanges: 0, locationChanged: false};
Sean Condonee545762019-03-09 10:43:58 +0000149 for (const key of Object.keys(updatedNode)) {
150 const o = updatedNode[key];
Sean Condon28884332019-03-21 14:07:00 +0000151 if (['id', 'x', 'y', 'fx', 'fy', 'vx', 'vy', 'index'].some(k => k === key)) {
Sean Condonee545762019-03-09 10:43:58 +0000152 continue;
153 } else if (o && typeof o === 'object' && o.constructor === Object) {
Sean Condon28884332019-03-21 14:07:00 +0000154 const subChanged = ForceSvgComponent.updateObject(existingNode[key], updatedNode[key]);
155 changed.numChanges += subChanged.numChanges;
156 changed.locationChanged = subChanged.locationChanged ? true : changed.locationChanged;
157 } else if (existingNode === undefined) {
158 // Copy the whole object
159 existingNode = updatedNode;
160 changed.locationChanged = true;
161 changed.numChanges++;
Sean Condonee545762019-03-09 10:43:58 +0000162 } else if (existingNode[key] !== updatedNode[key]) {
Sean Condon28884332019-03-21 14:07:00 +0000163 if (['locType', 'latOrY', 'longOrX', 'latitude', 'longitude', 'gridX', 'gridY'].some(k => k === key)) {
164 changed.locationChanged = true;
165 }
166 changed.numChanges++;
Sean Condonee545762019-03-09 10:43:58 +0000167 existingNode[key] = updatedNode[key];
168 }
169 }
170 return changed;
171 }
172
Sean Condon021f0fa2018-12-06 23:31:11 -0800173 @HostListener('window:resize', ['$event'])
174 onResize(event) {
Sean Condon28884332019-03-21 14:07:00 +0000175 this.graph.restartSimulation();
176 this.log.debug('Simulation restart after resize', event);
Sean Condon021f0fa2018-12-06 23:31:11 -0800177 }
178
179 /**
Sean Condon0c577f62018-11-18 22:40:05 +0000180 * After the component is initialized create the Force simulation
Sean Condon021f0fa2018-12-06 23:31:11 -0800181 * The list of devices, hosts and links will not have been receieved back
182 * from the WebSocket yet as this time - they will be updated later through
183 * ngOnChanges()
Sean Condon0c577f62018-11-18 22:40:05 +0000184 */
185 ngOnInit() {
186 // Receiving an initialized simulated graph from our custom d3 service
Sean Condon28884332019-03-21 14:07:00 +0000187 this.graph = new ForceDirectedGraph(SVGCANVAS, this.log);
Sean Condon0c577f62018-11-18 22:40:05 +0000188
189 /** Binding change detection check on each tick
Sean Condon021f0fa2018-12-06 23:31:11 -0800190 * This along with an onPush change detection strategy should enforce
191 * checking only when relevant! This improves scripting computation
192 * duration in a couple of tests I've made, consistently. Also, it makes
193 * sense to avoid unnecessary checks when we are dealing only with
194 * simulations data binding.
Sean Condon0c577f62018-11-18 22:40:05 +0000195 */
196 this.graph.ticker.subscribe((simulation) => {
Sean Condon28884332019-03-21 14:07:00 +0000197 // this.log.debug("Force simulation has ticked. Alpha",
198 // Math.round(simulation.alpha() * 1000) / 1000);
Sean Condon0c577f62018-11-18 22:40:05 +0000199 this.ref.markForCheck();
200 });
Sean Condon28884332019-03-21 14:07:00 +0000201
Sean Condon0c577f62018-11-18 22:40:05 +0000202 this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
203
Sean Condon0c577f62018-11-18 22:40:05 +0000204 }
205
206 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800207 * When any one of the inputs get changed by a containing component, this
208 * gets called automatically. In addition this is called manually by
209 * topology.service when a response is received from the WebSocket from the
210 * server
Sean Condon0c577f62018-11-18 22:40:05 +0000211 *
212 * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
213 * The Links are added to the Link list of the simulation.
214 * Before they are added the Links are associated with Nodes based on their endPt
215 *
216 * @param changes - a list of changed @Input(s)
217 */
218 ngOnChanges(changes: SimpleChanges) {
219 if (changes['regionData']) {
220 const devices: Device[] =
221 changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
222 const hosts: Host[] =
223 changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
224 const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
225 this.graph.nodes = [];
226 if (devices) {
227 this.graph.nodes = devices;
228 }
229 if (hosts) {
230 this.graph.nodes = this.graph.nodes.concat(hosts);
231 }
232 if (subRegions) {
233 this.graph.nodes = this.graph.nodes.concat(subRegions);
234 }
235
Sean Condon28884332019-03-21 14:07:00 +0000236 this.graph.nodes.forEach((n) => this.fixPosition(n));
Sean Condon71910542019-02-16 18:16:42 +0000237
Sean Condon0c577f62018-11-18 22:40:05 +0000238 // Associate the endpoints of each link with a real node
239 this.graph.links = [];
240 for (const linkIdx of Object.keys(this.regionData.links)) {
Sean Condon021f0fa2018-12-06 23:31:11 -0800241 const epA = ForceSvgComponent.extractNodeName(
242 this.regionData.links[linkIdx].epA);
Sean Condon0c577f62018-11-18 22:40:05 +0000243 this.regionData.links[linkIdx].source =
244 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800245 node.id === epA);
246 const epB = ForceSvgComponent.extractNodeName(
247 this.regionData.links[linkIdx].epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000248 this.regionData.links[linkIdx].target =
249 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800250 node.id === epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000251 this.regionData.links[linkIdx].index = Number(linkIdx);
252 }
253
254 this.graph.links = this.regionData.links;
Sean Condon28884332019-03-21 14:07:00 +0000255 if (this.graph.nodes.length > 0) {
256 this.graph.reinitSimulation();
257 }
Sean Condon0c577f62018-11-18 22:40:05 +0000258 this.log.debug('ForceSvgComponent input changed',
259 this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
260 }
Sean Condon28884332019-03-21 14:07:00 +0000261 }
Sean Condon021f0fa2018-12-06 23:31:11 -0800262
Sean Condon28884332019-03-21 14:07:00 +0000263 /**
264 * If a node has a fixed location then assign it to fx and fy so
265 * that it doesn't get affected by forces
266 * @param graphNode The node whose location should be processed
267 */
268 private fixPosition(graphNode: Node): void {
269 const loc: Location = <Location>graphNode['location'];
270 const props: DeviceProps = <DeviceProps>graphNode['props'];
271 const metaUi = <MetaUi>graphNode['metaUi'];
272 if (loc && loc.locType === LocationType.GEO) {
273 const position: MetaUi =
274 ZoomUtils.convertGeoToCanvas(
275 <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
276 graphNode.fx = position.x;
277 graphNode.fy = position.y;
278 this.log.debug('Found node', graphNode.id, 'with', loc.locType);
279 } else if (loc && loc.locType === LocationType.GRID) {
280 graphNode.fx = loc.longOrX;
281 graphNode.fy = loc.latOrY;
282 this.log.debug('Found node', graphNode.id, 'with', loc.locType);
283 } else if (props && props.locType === LocationType.NONE && metaUi) {
284 graphNode.fx = metaUi.x;
285 graphNode.fy = metaUi.y;
286 this.log.debug('Found node', graphNode.id, 'with locType=none and metaUi');
287 } else {
288 graphNode.fx = null;
289 graphNode.fy = null;
290 }
Sean Condon0c577f62018-11-18 22:40:05 +0000291 }
292
293 /**
294 * Get the index of LayerType so it can drive the visibility of nodes and
295 * hosts on layers
296 */
297 visibleLayerIdx(): number {
298 const layerKeys: string[] = Object.keys(LayerType);
299 for (const idx in layerKeys) {
300 if (LayerType[layerKeys[idx]] === this.visibleLayer) {
301 return Number(idx);
302 }
303 }
304 return -1;
305 }
306
307 selectLink(link: RegionLink): void {
308 this.selectedLink = link;
309 this.linkSelected.emit(link);
310 }
311
Sean Condon021f0fa2018-12-06 23:31:11 -0800312 /**
Sean Condond88f3662019-04-03 16:35:30 +0100313 * Iterate through all hosts and devices and links to deselect the previously selected
Sean Condon021f0fa2018-12-06 23:31:11 -0800314 * node. The emit an event to the parent that lets it know the selection has
315 * changed.
Sean Condond88f3662019-04-03 16:35:30 +0100316 *
317 * This function collates all of the nodes that have been selected and passes
318 * a collection of nodes up to the topology component
319 *
Sean Condon021f0fa2018-12-06 23:31:11 -0800320 * @param selectedNode the newly selected node
321 */
Sean Condond88f3662019-04-03 16:35:30 +0100322 updateSelected(selectedNode: SelectedEvent): void {
323 this.log.debug('Node or link ',
324 selectedNode.uiElement,
325 selectedNode.deselecting ? 'deselected' : 'selected',
326 selectedNode.isShift ? 'Multiple' : '');
Sean Condon021f0fa2018-12-06 23:31:11 -0800327
Sean Condond88f3662019-04-03 16:35:30 +0100328 if (selectedNode.isShift && selectedNode.deselecting) {
329 const idx = this.selectedNodes.findIndex((n) =>
330 n.id === selectedNode.uiElement.id
331 );
332 this.selectedNodes.splice(idx, 1);
333 this.log.debug('Removed node', idx);
334
335 } else if (selectedNode.isShift) {
336 this.selectedNodes.push(selectedNode.uiElement);
337
338 } else if (selectedNode.deselecting) {
339 this.selectedNodes = [];
340
341 } else {
342 const selNodeId = selectedNode.uiElement.id;
343 // Otherwise if shift was not pressed deselect previous
344 this.devices
345 .filter((d) => d.device.id !== selNodeId)
346 .forEach((d) => d.deselect());
347 this.hosts
348 .filter((h) => h.host.id !== selNodeId)
349 .forEach((h) => h.deselect());
350
351 this.links
352 .filter((l) => l.link.id !== selNodeId)
353 .forEach((l) => l.deselect());
354
355 this.selectedNodes = [selectedNode.uiElement];
356 }
Sean Condon91481822019-01-01 13:56:14 +0000357 // Push the changes back up to parent (Topology Component)
Sean Condond88f3662019-04-03 16:35:30 +0100358 this.selectedNodeEvent.emit(this.selectedNodes);
Sean Condon0c577f62018-11-18 22:40:05 +0000359 }
360
Sean Condon021f0fa2018-12-06 23:31:11 -0800361 /**
362 * We want to filter links to show only those not related to hosts if the
Sean Condon50855cf2018-12-23 15:37:42 +0000363 * 'showHosts' flag has been switched off. If 'showHosts' is true, then
Sean Condon021f0fa2018-12-06 23:31:11 -0800364 * display all links.
365 */
Sean Condon50855cf2018-12-23 15:37:42 +0000366 filteredLinks(): Link[] {
Sean Condon021f0fa2018-12-06 23:31:11 -0800367 return this.regionData.links.filter((h) =>
368 this.showHosts ||
369 ((<Host>h.source).nodeType !== 'host' &&
370 (<Host>h.target).nodeType !== 'host'));
Sean Condon0c577f62018-11-18 22:40:05 +0000371 }
Sean Condon50855cf2018-12-23 15:37:42 +0000372
373 /**
374 * When changes happen in the model, then model events are sent up through the
375 * Web Socket
376 * @param type - the type of the change
377 * @param memo - a qualifier on the type
378 * @param subject - the item that the update is for
379 * @param data - the new definition of the item
380 */
381 handleModelEvent(type: ModelEventType, memo: ModelEventMemo, subject: string, data: UiElement): void {
382 switch (type) {
383 case ModelEventType.DEVICE_ADDED_OR_UPDATED:
384 if (memo === ModelEventMemo.ADDED) {
Sean Condon28884332019-03-21 14:07:00 +0000385 this.fixPosition(<Device>data);
Sean Condonee545762019-03-09 10:43:58 +0000386 this.graph.nodes.push(<Device>data);
Sean Condonee5d4b92019-03-11 19:57:34 +0000387 this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
Sean Condonee545762019-03-09 10:43:58 +0000388 this.log.debug('Device added', (<Device>data).id);
Sean Condon50855cf2018-12-23 15:37:42 +0000389 } else if (memo === ModelEventMemo.UPDATED) {
390 const oldDevice: Device =
391 this.regionData.devices[this.visibleLayerIdx()]
392 .find((d) => d.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000393 const changes = ForceSvgComponent.updateObject(oldDevice, <Device>data);
Sean Condon28884332019-03-21 14:07:00 +0000394 if (changes.numChanges > 0) {
Sean Condonee545762019-03-09 10:43:58 +0000395 this.log.debug('Device ', oldDevice.id, memo, ' - ', changes, 'changes');
Sean Condon28884332019-03-21 14:07:00 +0000396 if (changes.locationChanged) {
397 this.fixPosition(oldDevice);
398 }
Sean Condonee545762019-03-09 10:43:58 +0000399 }
Sean Condon50855cf2018-12-23 15:37:42 +0000400 } else {
401 this.log.warn('Device ', memo, ' - not yet implemented', data);
402 }
Sean Condon50855cf2018-12-23 15:37:42 +0000403 break;
404 case ModelEventType.HOST_ADDED_OR_UPDATED:
405 if (memo === ModelEventMemo.ADDED) {
Sean Condon28884332019-03-21 14:07:00 +0000406 this.fixPosition(<Host>data);
Sean Condonee545762019-03-09 10:43:58 +0000407 this.graph.nodes.push(<Host>data);
Sean Condon28884332019-03-21 14:07:00 +0000408 this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
Sean Condonee545762019-03-09 10:43:58 +0000409 this.log.debug('Host added', (<Host>data).id);
Sean Condon50855cf2018-12-23 15:37:42 +0000410 } else if (memo === ModelEventMemo.UPDATED) {
411 const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
412 .find((h) => h.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000413 const changes = ForceSvgComponent.updateObject(oldHost, <Host>data);
Sean Condon28884332019-03-21 14:07:00 +0000414 if (changes.numChanges > 0) {
Sean Condonee545762019-03-09 10:43:58 +0000415 this.log.debug('Host ', oldHost.id, memo, ' - ', changes, 'changes');
Sean Condon28884332019-03-21 14:07:00 +0000416 if (changes.locationChanged) {
417 this.fixPosition(oldHost);
418 }
Sean Condonee545762019-03-09 10:43:58 +0000419 }
Sean Condon50855cf2018-12-23 15:37:42 +0000420 } else {
421 this.log.warn('Host change', memo, ' - unexpected');
422 }
423 break;
424 case ModelEventType.DEVICE_REMOVED:
425 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
426 const removeIdx: number =
427 this.regionData.devices[this.visibleLayerIdx()]
428 .findIndex((d) => d.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000429 this.regionData.devices[this.visibleLayerIdx()].splice(removeIdx, 1);
430 this.removeRelatedLinks(subject);
431 this.log.debug('Device ', subject, 'removed. Links', this.regionData.links);
Sean Condon50855cf2018-12-23 15:37:42 +0000432 } else {
433 this.log.warn('Device removed - unexpected memo', memo);
434 }
435 break;
436 case ModelEventType.HOST_REMOVED:
437 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
438 const removeIdx: number =
439 this.regionData.hosts[this.visibleLayerIdx()]
440 .findIndex((h) => h.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000441 this.regionData.hosts[this.visibleLayerIdx()].splice(removeIdx, 1);
442 this.removeRelatedLinks(subject);
443 this.log.warn('Host ', subject, 'removed');
Sean Condon50855cf2018-12-23 15:37:42 +0000444 } else {
445 this.log.warn('Host removed - unexpected memo', memo);
446 }
447 break;
448 case ModelEventType.LINK_ADDED_OR_UPDATED:
Sean Condonee545762019-03-09 10:43:58 +0000449 if (memo === ModelEventMemo.ADDED &&
450 this.regionData.links.findIndex((l) => l.id === subject) === -1) {
451 const listLen = this.regionData.links.push(<RegionLink>data);
452 const epA = ForceSvgComponent.extractNodeName(
453 this.regionData.links[listLen - 1].epA);
454 this.regionData.links[listLen - 1].source =
455 this.graph.nodes.find((node) =>
456 node.id === epA);
457 const epB = ForceSvgComponent.extractNodeName(
458 this.regionData.links[listLen - 1].epB);
459 this.regionData.links[listLen - 1].target =
460 this.graph.nodes.find((node) =>
461 node.id === epB);
462 this.log.debug('Link added', subject);
463 } else if (memo === ModelEventMemo.UPDATED) {
464 const oldLink = this.regionData.links.find((l) => l.id === subject);
465 const changes = ForceSvgComponent.updateObject(oldLink, <RegionLink>data);
466 this.log.debug('Link ', subject, '. Updated', changes, 'items');
467 } else {
468 this.log.warn('Link added or updated - unexpected memo', memo);
469 }
Sean Condon50855cf2018-12-23 15:37:42 +0000470 break;
471 default:
472 this.log.error('Unexpected model event', type, 'for', subject);
473 }
Sean Condon28884332019-03-21 14:07:00 +0000474 this.graph.links = this.regionData.links;
475 this.graph.reinitSimulation();
Sean Condon50855cf2018-12-23 15:37:42 +0000476 }
477
Sean Condonee545762019-03-09 10:43:58 +0000478 private removeRelatedLinks(subject: string) {
479 const len = this.regionData.links.length;
480 for (let i = 0; i < len; i++) {
481 const linkIdx = this.regionData.links.findIndex((l) =>
482 (ForceSvgComponent.extractNodeName(l.epA) === subject ||
483 ForceSvgComponent.extractNodeName(l.epB) === subject));
484 if (linkIdx >= 0) {
485 this.regionData.links.splice(linkIdx, 1);
486 this.log.debug('Link ', linkIdx, 'removed on attempt', i);
487 }
Sean Condon50855cf2018-12-23 15:37:42 +0000488 }
489 }
490
491 /**
492 * When traffic monitoring is turned on (A key) highlights will be sent back
493 * from the WebSocket through the Traffic Service
494 * @param devices - an array of device highlights
495 * @param hosts - an array of host highlights
496 * @param links - an array of link highlights
497 */
498 handleHighlights(devices: Device[], hosts: Host[], links: LinkHighlight[]): void {
499
500 if (devices.length > 0) {
501 this.log.debug(devices.length, 'Devices highlighted');
502 devices.forEach((dh) => {
503 const deviceComponent: DeviceNodeSvgComponent = this.devices.find((d) => d.device.id === dh.id );
504 if (deviceComponent) {
505 deviceComponent.ngOnChanges(
506 {'deviceHighlight': new SimpleChange(<Device>{}, dh, true)}
507 );
508 this.log.debug('Highlighting device', deviceComponent.device.id);
509 } else {
510 this.log.warn('Device component not found', dh.id);
511 }
512 });
513 }
514 if (hosts.length > 0) {
515 this.log.debug(hosts.length, 'Hosts highlighted');
516 hosts.forEach((hh) => {
517 const hostComponent: HostNodeSvgComponent = this.hosts.find((h) => h.host.id === hh.id );
518 if (hostComponent) {
519 hostComponent.ngOnChanges(
520 {'hostHighlight': new SimpleChange(<Host>{}, hh, true)}
521 );
522 this.log.debug('Highlighting host', hostComponent.host.id);
523 }
524 });
525 }
526 if (links.length > 0) {
527 this.log.debug(links.length, 'Links highlighted');
528 links.forEach((lh) => {
529 const linkComponent: LinkSvgComponent = this.links.find((l) => l.link.id === lh.id );
530 if (linkComponent) { // A link might not be present is hosts viewing is switched off
531 linkComponent.ngOnChanges(
532 {'linkHighlight': new SimpleChange(<LinkHighlight>{}, lh, true)}
533 );
534 // this.log.debug('Highlighting link', linkComponent.link.id, lh.css, lh.label);
535 }
536 });
537 }
538 }
Sean Condon71910542019-02-16 18:16:42 +0000539
540 /**
541 * As nodes are dragged around the graph, their new location should be sent
542 * back to server
543 * @param klass The class of node e.g. 'host' or 'device'
544 * @param id - the ID of the node
545 * @param newLocation - the new Location of the node
546 */
547 nodeMoved(klass: string, id: string, newLocation: MetaUi) {
Sean Condon28884332019-03-21 14:07:00 +0000548 this.wss.sendEvent('updateMeta2', <UpdateMeta>{
Sean Condon71910542019-02-16 18:16:42 +0000549 id: id,
550 class: klass,
551 memento: newLocation
552 });
553 this.log.debug(klass, id, 'has been moved to', newLocation);
554 }
Sean Condon1ae15802019-03-02 09:07:18 +0000555
Sean Condon9de21352019-04-06 19:22:27 +0100556 /**
557 * If any nodes with fixed positions had been dragged out of place
558 * then put back where they belong
559 * If there are some devices selected reset only these
560 */
561 resetNodeLocations(): number {
562 let numbernodes = 0;
563 if (this.selectedNodes.length > 0) {
564 this.devices
565 .filter((d) => this.selectedNodes.some((s) => s.id === d.device.id))
566 .forEach((dev) => {
567 Node.resetNodeLocation(<Node>dev.device);
568 numbernodes++;
569 });
570 this.hosts
571 .filter((h) => this.selectedNodes.some((s) => s.id === h.host.id))
572 .forEach((h) => {
573 Host.resetNodeLocation(<Host>h.host);
574 numbernodes++;
575 });
576 } else {
577 this.devices.forEach((dev) => {
578 Node.resetNodeLocation(<Node>dev.device);
579 numbernodes++;
580 });
581 this.hosts.forEach((h) => {
582 Host.resetNodeLocation(<Host>h.host);
583 numbernodes++;
584 });
585 }
586 this.graph.reinitSimulation();
587 return numbernodes;
Sean Condon1ae15802019-03-02 09:07:18 +0000588 }
Sean Condon9de21352019-04-06 19:22:27 +0100589
590 /**
591 * Toggle floating nodes between unpinned and frozen
592 * There may be frozen and unpinned in the selection
593 *
594 * If there are nodes selected toggle only these
595 */
596 unpinOrFreezeNodes(freeze: boolean): number {
597 let numbernodes = 0;
598 if (this.selectedNodes.length > 0) {
599 this.devices
600 .filter((d) => this.selectedNodes.some((s) => s.id === d.device.id))
601 .forEach((d) => {
602 Node.unpinOrFreezeNode(<Node>d.device, freeze);
603 numbernodes++;
604 });
605 this.hosts
606 .filter((h) => this.selectedNodes.some((s) => s.id === h.host.id))
607 .forEach((h) => {
608 Node.unpinOrFreezeNode(<Node>h.host, freeze);
609 numbernodes++;
610 });
611 } else {
612 this.devices.forEach((d) => {
613 Node.unpinOrFreezeNode(<Node>d.device, freeze);
614 numbernodes++;
615 });
616 this.hosts.forEach((h) => {
617 Node.unpinOrFreezeNode(<Node>h.host, freeze);
618 numbernodes++;
619 });
620 }
621 this.graph.reinitSimulation();
622 return numbernodes;
623 }
624
Sean Condonf4f54a12018-10-10 23:25:46 +0100625}
Sean Condon0c577f62018-11-18 22:40:05 +0000626