blob: adc814ed347c6f777fe7b18a76614ea62dc0bcfe [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 Condon058804c2019-04-16 09:41:52 +010031import {LocMeta, LogService, MetaUi, SvgUtilService, WebSocketService, ZoomUtils} from 'gui2-fw-lib';
Sean Condon1ae15802019-03-02 09:07:18 +000032import {
Sean Condon5f7d3bc2019-04-15 11:18:34 +010033 Device,
34 DeviceProps,
Sean Condon0c577f62018-11-18 22:40:05 +000035 ForceDirectedGraph,
Sean Condon50855cf2018-12-23 15:37:42 +000036 Host,
37 HostLabelToggle,
Sean Condon0c577f62018-11-18 22:40:05 +000038 LabelToggle,
39 LayerType,
Sean Condon50855cf2018-12-23 15:37:42 +000040 Link,
41 LinkHighlight,
Sean Condon1ae15802019-03-02 09:07:18 +000042 Location,
Sean Condon50855cf2018-12-23 15:37:42 +000043 ModelEventMemo,
44 ModelEventType,
Sean Condon28884332019-03-21 14:07:00 +000045 Node,
Sean Condond88f3662019-04-03 16:35:30 +010046 Options,
Sean Condon0c577f62018-11-18 22:40:05 +000047 Region,
48 RegionLink,
Sean Condon50855cf2018-12-23 15:37:42 +000049 SubRegion,
50 UiElement
Sean Condon0c577f62018-11-18 22:40:05 +000051} from './models';
Sean Condonee545762019-03-09 10:43:58 +000052import {LocationType} from '../backgroundsvg/backgroundsvg.component';
Sean Condonff85fbe2019-03-16 14:28:46 +000053import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
Sean Condon5f7d3bc2019-04-15 11:18:34 +010054import {HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
55import {LinkSvgComponent} from './visuals/linksvg/linksvg.component';
Sean Condond88f3662019-04-03 16:35:30 +010056import {SelectedEvent} from './visuals/nodevisual';
Sean Condon71910542019-02-16 18:16:42 +000057
58interface UpdateMeta {
59 id: string;
60 class: string;
61 memento: MetaUi;
62}
Sean Condonaa4366d2018-11-02 14:29:01 +000063
Sean Condon28884332019-03-21 14:07:00 +000064const SVGCANVAS = <Options>{
65 width: 1000,
66 height: 1000
67};
68
69interface ChangeSummary {
70 numChanges: number;
71 locationChanged: boolean;
72}
73
Sean Condonf4f54a12018-10-10 23:25:46 +010074/**
75 * ONOS GUI -- Topology Forces Graph Layer View.
Sean Condon0c577f62018-11-18 22:40:05 +000076 *
77 * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
78 * This drives the whole Force graph
Sean Condonf4f54a12018-10-10 23:25:46 +010079 */
80@Component({
81 selector: '[onos-forcesvg]',
82 templateUrl: './forcesvg.component.html',
Sean Condon0c577f62018-11-18 22:40:05 +000083 styleUrls: ['./forcesvg.component.css'],
84 changeDetection: ChangeDetectionStrategy.OnPush,
Sean Condonf4f54a12018-10-10 23:25:46 +010085})
Sean Condon0c577f62018-11-18 22:40:05 +000086export class ForceSvgComponent implements OnInit, OnChanges {
Sean Condonff85fbe2019-03-16 14:28:46 +000087 @Input() deviceLabelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
88 @Input() hostLabelToggle: HostLabelToggle.Enum = HostLabelToggle.Enum.NONE;
Sean Condonb2c483c2019-01-16 20:28:55 +000089 @Input() showHosts: boolean = false;
90 @Input() highlightPorts: boolean = true;
91 @Input() onosInstMastership: string = '';
92 @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
93 @Input() selectedLink: RegionLink = null;
Sean Condon1ae15802019-03-02 09:07:18 +000094 @Input() scale: number = 1;
Sean Condon0c577f62018-11-18 22:40:05 +000095 @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
Sean Condonb2c483c2019-01-16 20:28:55 +000096 @Output() linkSelected = new EventEmitter<RegionLink>();
Sean Condond88f3662019-04-03 16:35:30 +010097 @Output() selectedNodeEvent = new EventEmitter<UiElement[]>();
Sean Condonff85fbe2019-03-16 14:28:46 +000098 public graph: ForceDirectedGraph;
Sean Condond88f3662019-04-03 16:35:30 +010099 private selectedNodes: UiElement[] = [];
Sean Condonf4f54a12018-10-10 23:25:46 +0100100
Sean Condon021f0fa2018-12-06 23:31:11 -0800101 // References to the children of this component - these are created in the
102 // template view with the *ngFor and we get them by a query here
Sean Condon0c577f62018-11-18 22:40:05 +0000103 @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
Sean Condon021f0fa2018-12-06 23:31:11 -0800104 @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
Sean Condon50855cf2018-12-23 15:37:42 +0000105 @ViewChildren(LinkSvgComponent) links: QueryList<LinkSvgComponent>;
Sean Condonf4f54a12018-10-10 23:25:46 +0100106
Sean Condon0c577f62018-11-18 22:40:05 +0000107 constructor(
108 protected log: LogService,
Sean Condon71910542019-02-16 18:16:42 +0000109 private ref: ChangeDetectorRef,
110 protected wss: WebSocketService
Sean Condon0c577f62018-11-18 22:40:05 +0000111 ) {
112 this.selectedLink = null;
113 this.log.debug('ForceSvgComponent constructed');
114 }
115
116 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800117 * Utility for extracting a node name from an endpoint string
118 * In some cases - have to remove the port number from the end of a device
119 * name
120 * @param endPtStr The end point name
121 */
Sean Condonb77768e2019-05-04 20:23:42 +0100122 static extractNodeName(endPtStr: string, portStr: string): string {
123 if (portStr === undefined || endPtStr === undefined) {
Sean Condon021f0fa2018-12-06 23:31:11 -0800124 return endPtStr;
Sean Condonb77768e2019-05-04 20:23:42 +0100125 } else if (endPtStr.includes('/')) {
126 return endPtStr.substr(0, endPtStr.length - portStr.length - 1);
Sean Condon021f0fa2018-12-06 23:31:11 -0800127 }
Sean Condonb77768e2019-05-04 20:23:42 +0100128 return endPtStr;
Sean Condon021f0fa2018-12-06 23:31:11 -0800129 }
130
Sean Condonee545762019-03-09 10:43:58 +0000131 /**
132 * Recursive method to compare 2 objects attribute by attribute and update
133 * the first where a change is detected
134 * @param existingNode 1st object
135 * @param updatedNode 2nd object
136 */
Sean Condon28884332019-03-21 14:07:00 +0000137 private static updateObject(existingNode: Object, updatedNode: Object): ChangeSummary {
138 const changed = <ChangeSummary>{numChanges: 0, locationChanged: false};
Sean Condonee545762019-03-09 10:43:58 +0000139 for (const key of Object.keys(updatedNode)) {
140 const o = updatedNode[key];
Sean Condon28884332019-03-21 14:07:00 +0000141 if (['id', 'x', 'y', 'fx', 'fy', 'vx', 'vy', 'index'].some(k => k === key)) {
Sean Condonee545762019-03-09 10:43:58 +0000142 continue;
143 } else if (o && typeof o === 'object' && o.constructor === Object) {
Sean Condon28884332019-03-21 14:07:00 +0000144 const subChanged = ForceSvgComponent.updateObject(existingNode[key], updatedNode[key]);
145 changed.numChanges += subChanged.numChanges;
146 changed.locationChanged = subChanged.locationChanged ? true : changed.locationChanged;
147 } else if (existingNode === undefined) {
148 // Copy the whole object
149 existingNode = updatedNode;
150 changed.locationChanged = true;
151 changed.numChanges++;
Sean Condonee545762019-03-09 10:43:58 +0000152 } else if (existingNode[key] !== updatedNode[key]) {
Sean Condon28884332019-03-21 14:07:00 +0000153 if (['locType', 'latOrY', 'longOrX', 'latitude', 'longitude', 'gridX', 'gridY'].some(k => k === key)) {
154 changed.locationChanged = true;
155 }
156 changed.numChanges++;
Sean Condonee545762019-03-09 10:43:58 +0000157 existingNode[key] = updatedNode[key];
158 }
159 }
160 return changed;
161 }
162
Sean Condon021f0fa2018-12-06 23:31:11 -0800163 @HostListener('window:resize', ['$event'])
164 onResize(event) {
Sean Condon28884332019-03-21 14:07:00 +0000165 this.graph.restartSimulation();
166 this.log.debug('Simulation restart after resize', event);
Sean Condon021f0fa2018-12-06 23:31:11 -0800167 }
168
169 /**
Sean Condon0c577f62018-11-18 22:40:05 +0000170 * After the component is initialized create the Force simulation
Sean Condon021f0fa2018-12-06 23:31:11 -0800171 * The list of devices, hosts and links will not have been receieved back
172 * from the WebSocket yet as this time - they will be updated later through
173 * ngOnChanges()
Sean Condon0c577f62018-11-18 22:40:05 +0000174 */
175 ngOnInit() {
176 // Receiving an initialized simulated graph from our custom d3 service
Sean Condon28884332019-03-21 14:07:00 +0000177 this.graph = new ForceDirectedGraph(SVGCANVAS, this.log);
Sean Condon0c577f62018-11-18 22:40:05 +0000178
179 /** Binding change detection check on each tick
Sean Condon021f0fa2018-12-06 23:31:11 -0800180 * This along with an onPush change detection strategy should enforce
181 * checking only when relevant! This improves scripting computation
182 * duration in a couple of tests I've made, consistently. Also, it makes
183 * sense to avoid unnecessary checks when we are dealing only with
184 * simulations data binding.
Sean Condon0c577f62018-11-18 22:40:05 +0000185 */
186 this.graph.ticker.subscribe((simulation) => {
Sean Condon28884332019-03-21 14:07:00 +0000187 // this.log.debug("Force simulation has ticked. Alpha",
188 // Math.round(simulation.alpha() * 1000) / 1000);
Sean Condon0c577f62018-11-18 22:40:05 +0000189 this.ref.markForCheck();
190 });
Sean Condon28884332019-03-21 14:07:00 +0000191
Sean Condon0c577f62018-11-18 22:40:05 +0000192 this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
193
Sean Condon0c577f62018-11-18 22:40:05 +0000194 }
195
196 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800197 * When any one of the inputs get changed by a containing component, this
198 * gets called automatically. In addition this is called manually by
199 * topology.service when a response is received from the WebSocket from the
200 * server
Sean Condon0c577f62018-11-18 22:40:05 +0000201 *
202 * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
203 * The Links are added to the Link list of the simulation.
204 * Before they are added the Links are associated with Nodes based on their endPt
205 *
206 * @param changes - a list of changed @Input(s)
207 */
208 ngOnChanges(changes: SimpleChanges) {
209 if (changes['regionData']) {
210 const devices: Device[] =
211 changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
212 const hosts: Host[] =
213 changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
214 const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
215 this.graph.nodes = [];
216 if (devices) {
217 this.graph.nodes = devices;
218 }
219 if (hosts) {
220 this.graph.nodes = this.graph.nodes.concat(hosts);
221 }
222 if (subRegions) {
223 this.graph.nodes = this.graph.nodes.concat(subRegions);
224 }
225
Sean Condon28884332019-03-21 14:07:00 +0000226 this.graph.nodes.forEach((n) => this.fixPosition(n));
Sean Condon71910542019-02-16 18:16:42 +0000227
Sean Condon0c577f62018-11-18 22:40:05 +0000228 // Associate the endpoints of each link with a real node
229 this.graph.links = [];
230 for (const linkIdx of Object.keys(this.regionData.links)) {
Sean Condonb77768e2019-05-04 20:23:42 +0100231 const link = this.regionData.links[linkIdx];
232 const epA = ForceSvgComponent.extractNodeName(link.epA, link.portA);
233 if (!this.graph.nodes.find((node) => node.id === epA)) {
234 this.log.error('ngOnChange Could not find endpoint A', epA, 'for', link);
235 continue;
236 }
237 const epB = ForceSvgComponent.extractNodeName(
238 link.epB, link.portB);
239 if (!this.graph.nodes.find((node) => node.id === epB)) {
240 this.log.error('ngOnChange Could not find endpoint B', epB, 'for', link);
241 continue;
242 }
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);
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 /**
Sean Condon058804c2019-04-16 09:41:52 +0100262 * If instance has a value then mute colors of devices not connected to it
263 * Otherwise if instance does not have a value unmute all
264 * @param instanceName name of the selected instance
265 */
266 changeInstSelection(instanceName: string) {
267 this.log.debug('Mastership changed', instanceName);
268 this.devices.filter((d) => d.device.master !== instanceName)
269 .forEach((d) => {
270 const isMuted = Boolean(instanceName);
271 d.ngOnChanges({'colorMuted': new SimpleChange(!isMuted, isMuted, true)});
272 }
273 );
274 }
275
276 /**
Sean Condon28884332019-03-21 14:07:00 +0000277 * If a node has a fixed location then assign it to fx and fy so
278 * that it doesn't get affected by forces
279 * @param graphNode The node whose location should be processed
280 */
281 private fixPosition(graphNode: Node): void {
282 const loc: Location = <Location>graphNode['location'];
283 const props: DeviceProps = <DeviceProps>graphNode['props'];
284 const metaUi = <MetaUi>graphNode['metaUi'];
285 if (loc && loc.locType === LocationType.GEO) {
286 const position: MetaUi =
287 ZoomUtils.convertGeoToCanvas(
288 <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
289 graphNode.fx = position.x;
290 graphNode.fy = position.y;
291 this.log.debug('Found node', graphNode.id, 'with', loc.locType);
292 } else if (loc && loc.locType === LocationType.GRID) {
293 graphNode.fx = loc.longOrX;
294 graphNode.fy = loc.latOrY;
295 this.log.debug('Found node', graphNode.id, 'with', loc.locType);
296 } else if (props && props.locType === LocationType.NONE && metaUi) {
297 graphNode.fx = metaUi.x;
298 graphNode.fy = metaUi.y;
299 this.log.debug('Found node', graphNode.id, 'with locType=none and metaUi');
300 } else {
301 graphNode.fx = null;
302 graphNode.fy = null;
303 }
Sean Condon0c577f62018-11-18 22:40:05 +0000304 }
305
306 /**
307 * Get the index of LayerType so it can drive the visibility of nodes and
308 * hosts on layers
309 */
310 visibleLayerIdx(): number {
311 const layerKeys: string[] = Object.keys(LayerType);
312 for (const idx in layerKeys) {
313 if (LayerType[layerKeys[idx]] === this.visibleLayer) {
314 return Number(idx);
315 }
316 }
317 return -1;
318 }
319
320 selectLink(link: RegionLink): void {
321 this.selectedLink = link;
322 this.linkSelected.emit(link);
323 }
324
Sean Condon021f0fa2018-12-06 23:31:11 -0800325 /**
Sean Condond88f3662019-04-03 16:35:30 +0100326 * Iterate through all hosts and devices and links to deselect the previously selected
Sean Condon021f0fa2018-12-06 23:31:11 -0800327 * node. The emit an event to the parent that lets it know the selection has
328 * changed.
Sean Condond88f3662019-04-03 16:35:30 +0100329 *
330 * This function collates all of the nodes that have been selected and passes
331 * a collection of nodes up to the topology component
332 *
Sean Condon021f0fa2018-12-06 23:31:11 -0800333 * @param selectedNode the newly selected node
334 */
Sean Condond88f3662019-04-03 16:35:30 +0100335 updateSelected(selectedNode: SelectedEvent): void {
336 this.log.debug('Node or link ',
Sean Condon64ea7d22019-04-12 19:39:13 +0100337 selectedNode.uiElement ? selectedNode.uiElement.id : '--',
Sean Condond88f3662019-04-03 16:35:30 +0100338 selectedNode.deselecting ? 'deselected' : 'selected',
339 selectedNode.isShift ? 'Multiple' : '');
Sean Condon021f0fa2018-12-06 23:31:11 -0800340
Sean Condond88f3662019-04-03 16:35:30 +0100341 if (selectedNode.isShift && selectedNode.deselecting) {
342 const idx = this.selectedNodes.findIndex((n) =>
343 n.id === selectedNode.uiElement.id
344 );
345 this.selectedNodes.splice(idx, 1);
346 this.log.debug('Removed node', idx);
347
348 } else if (selectedNode.isShift) {
349 this.selectedNodes.push(selectedNode.uiElement);
350
351 } else if (selectedNode.deselecting) {
Sean Condon64ea7d22019-04-12 19:39:13 +0100352 this.devices
353 .forEach((d) => d.deselect());
354 this.hosts
355 .forEach((h) => h.deselect());
356 this.links
357 .forEach((l) => l.deselect());
Sean Condond88f3662019-04-03 16:35:30 +0100358 this.selectedNodes = [];
359
360 } else {
361 const selNodeId = selectedNode.uiElement.id;
362 // Otherwise if shift was not pressed deselect previous
363 this.devices
364 .filter((d) => d.device.id !== selNodeId)
365 .forEach((d) => d.deselect());
366 this.hosts
367 .filter((h) => h.host.id !== selNodeId)
368 .forEach((h) => h.deselect());
369
370 this.links
371 .filter((l) => l.link.id !== selNodeId)
372 .forEach((l) => l.deselect());
373
374 this.selectedNodes = [selectedNode.uiElement];
375 }
Sean Condon91481822019-01-01 13:56:14 +0000376 // Push the changes back up to parent (Topology Component)
Sean Condond88f3662019-04-03 16:35:30 +0100377 this.selectedNodeEvent.emit(this.selectedNodes);
Sean Condon0c577f62018-11-18 22:40:05 +0000378 }
379
Sean Condon021f0fa2018-12-06 23:31:11 -0800380 /**
381 * We want to filter links to show only those not related to hosts if the
Sean Condon50855cf2018-12-23 15:37:42 +0000382 * 'showHosts' flag has been switched off. If 'showHosts' is true, then
Sean Condon021f0fa2018-12-06 23:31:11 -0800383 * display all links.
384 */
Sean Condon50855cf2018-12-23 15:37:42 +0000385 filteredLinks(): Link[] {
Sean Condon021f0fa2018-12-06 23:31:11 -0800386 return this.regionData.links.filter((h) =>
387 this.showHosts ||
388 ((<Host>h.source).nodeType !== 'host' &&
389 (<Host>h.target).nodeType !== 'host'));
Sean Condon0c577f62018-11-18 22:40:05 +0000390 }
Sean Condon50855cf2018-12-23 15:37:42 +0000391
392 /**
393 * When changes happen in the model, then model events are sent up through the
394 * Web Socket
395 * @param type - the type of the change
396 * @param memo - a qualifier on the type
397 * @param subject - the item that the update is for
398 * @param data - the new definition of the item
399 */
400 handleModelEvent(type: ModelEventType, memo: ModelEventMemo, subject: string, data: UiElement): void {
401 switch (type) {
402 case ModelEventType.DEVICE_ADDED_OR_UPDATED:
403 if (memo === ModelEventMemo.ADDED) {
Sean Condon28884332019-03-21 14:07:00 +0000404 this.fixPosition(<Device>data);
Sean Condonee545762019-03-09 10:43:58 +0000405 this.graph.nodes.push(<Device>data);
Sean Condonee5d4b92019-03-11 19:57:34 +0000406 this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
Sean Condonee545762019-03-09 10:43:58 +0000407 this.log.debug('Device added', (<Device>data).id);
Sean Condon50855cf2018-12-23 15:37:42 +0000408 } else if (memo === ModelEventMemo.UPDATED) {
409 const oldDevice: Device =
410 this.regionData.devices[this.visibleLayerIdx()]
411 .find((d) => d.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000412 const changes = ForceSvgComponent.updateObject(oldDevice, <Device>data);
Sean Condon28884332019-03-21 14:07:00 +0000413 if (changes.numChanges > 0) {
Sean Condonee545762019-03-09 10:43:58 +0000414 this.log.debug('Device ', oldDevice.id, memo, ' - ', changes, 'changes');
Sean Condon28884332019-03-21 14:07:00 +0000415 if (changes.locationChanged) {
416 this.fixPosition(oldDevice);
417 }
Sean Condon058804c2019-04-16 09:41:52 +0100418 const svgDevice: DeviceNodeSvgComponent =
419 this.devices.find((svgdevice) => svgdevice.device.id === subject);
420 svgDevice.ngOnChanges({'device':
421 new SimpleChange(<Device>{}, oldDevice, true)
422 });
Sean Condonee545762019-03-09 10:43:58 +0000423 }
Sean Condon50855cf2018-12-23 15:37:42 +0000424 } else {
425 this.log.warn('Device ', memo, ' - not yet implemented', data);
426 }
Sean Condon50855cf2018-12-23 15:37:42 +0000427 break;
428 case ModelEventType.HOST_ADDED_OR_UPDATED:
429 if (memo === ModelEventMemo.ADDED) {
Sean Condon28884332019-03-21 14:07:00 +0000430 this.fixPosition(<Host>data);
Sean Condonee545762019-03-09 10:43:58 +0000431 this.graph.nodes.push(<Host>data);
Sean Condon28884332019-03-21 14:07:00 +0000432 this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
Sean Condonee545762019-03-09 10:43:58 +0000433 this.log.debug('Host added', (<Host>data).id);
Sean Condon50855cf2018-12-23 15:37:42 +0000434 } else if (memo === ModelEventMemo.UPDATED) {
435 const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
436 .find((h) => h.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000437 const changes = ForceSvgComponent.updateObject(oldHost, <Host>data);
Sean Condon28884332019-03-21 14:07:00 +0000438 if (changes.numChanges > 0) {
Sean Condonee545762019-03-09 10:43:58 +0000439 this.log.debug('Host ', oldHost.id, memo, ' - ', changes, 'changes');
Sean Condon28884332019-03-21 14:07:00 +0000440 if (changes.locationChanged) {
441 this.fixPosition(oldHost);
442 }
Sean Condonee545762019-03-09 10:43:58 +0000443 }
Sean Condon50855cf2018-12-23 15:37:42 +0000444 } else {
445 this.log.warn('Host change', memo, ' - unexpected');
446 }
447 break;
448 case ModelEventType.DEVICE_REMOVED:
449 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
450 const removeIdx: number =
451 this.regionData.devices[this.visibleLayerIdx()]
452 .findIndex((d) => d.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000453 this.regionData.devices[this.visibleLayerIdx()].splice(removeIdx, 1);
454 this.removeRelatedLinks(subject);
455 this.log.debug('Device ', subject, 'removed. Links', this.regionData.links);
Sean Condon50855cf2018-12-23 15:37:42 +0000456 } else {
457 this.log.warn('Device removed - unexpected memo', memo);
458 }
459 break;
460 case ModelEventType.HOST_REMOVED:
461 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
462 const removeIdx: number =
463 this.regionData.hosts[this.visibleLayerIdx()]
464 .findIndex((h) => h.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000465 this.regionData.hosts[this.visibleLayerIdx()].splice(removeIdx, 1);
466 this.removeRelatedLinks(subject);
Sean Condon5f7d3bc2019-04-15 11:18:34 +0100467 this.log.debug('Host ', subject, 'removed');
Sean Condon50855cf2018-12-23 15:37:42 +0000468 } else {
469 this.log.warn('Host removed - unexpected memo', memo);
470 }
471 break;
472 case ModelEventType.LINK_ADDED_OR_UPDATED:
Sean Condonee545762019-03-09 10:43:58 +0000473 if (memo === ModelEventMemo.ADDED &&
474 this.regionData.links.findIndex((l) => l.id === subject) === -1) {
Sean Condonb77768e2019-05-04 20:23:42 +0100475 const newLink = <RegionLink>data;
476
477
Sean Condonee545762019-03-09 10:43:58 +0000478 const epA = ForceSvgComponent.extractNodeName(
Sean Condonb77768e2019-05-04 20:23:42 +0100479 newLink.epA, newLink.portA);
480 if (!this.graph.nodes.find((node) => node.id === epA)) {
481 this.log.error('Could not find endpoint A', epA, 'of', newLink);
482 break;
483 }
Sean Condonee545762019-03-09 10:43:58 +0000484 const epB = ForceSvgComponent.extractNodeName(
Sean Condonb77768e2019-05-04 20:23:42 +0100485 newLink.epB, newLink.portB);
486 if (!this.graph.nodes.find((node) => node.id === epB)) {
487 this.log.error('Could not find endpoint B', epB, 'of link', newLink);
488 break;
489 }
490
491 const listLen = this.regionData.links.push(<RegionLink>data);
492 this.regionData.links[listLen - 1].source =
493 this.graph.nodes.find((node) => node.id === epA);
Sean Condonee545762019-03-09 10:43:58 +0000494 this.regionData.links[listLen - 1].target =
Sean Condonb77768e2019-05-04 20:23:42 +0100495 this.graph.nodes.find((node) => node.id === epB);
Sean Condonee545762019-03-09 10:43:58 +0000496 this.log.debug('Link added', subject);
497 } else if (memo === ModelEventMemo.UPDATED) {
498 const oldLink = this.regionData.links.find((l) => l.id === subject);
499 const changes = ForceSvgComponent.updateObject(oldLink, <RegionLink>data);
500 this.log.debug('Link ', subject, '. Updated', changes, 'items');
501 } else {
Sean Condon5f7d3bc2019-04-15 11:18:34 +0100502 this.log.warn('Link event ignored', subject, data);
503 }
504 break;
505 case ModelEventType.LINK_REMOVED:
506 if (memo === ModelEventMemo.REMOVED) {
507 const removeIdx = this.regionData.links.findIndex((l) => l.id === subject);
508 this.regionData.links.splice(removeIdx, 1);
509 this.log.debug('Link ', subject, 'removed');
Sean Condonee545762019-03-09 10:43:58 +0000510 }
Sean Condon50855cf2018-12-23 15:37:42 +0000511 break;
512 default:
Sean Condon5f7d3bc2019-04-15 11:18:34 +0100513 this.log.error('Unexpected model event', type, 'for', subject, 'Data', data);
Sean Condon50855cf2018-12-23 15:37:42 +0000514 }
Sean Condon28884332019-03-21 14:07:00 +0000515 this.graph.links = this.regionData.links;
516 this.graph.reinitSimulation();
Sean Condon50855cf2018-12-23 15:37:42 +0000517 }
518
Sean Condonee545762019-03-09 10:43:58 +0000519 private removeRelatedLinks(subject: string) {
520 const len = this.regionData.links.length;
521 for (let i = 0; i < len; i++) {
522 const linkIdx = this.regionData.links.findIndex((l) =>
Sean Condonb77768e2019-05-04 20:23:42 +0100523 (ForceSvgComponent.extractNodeName(l.epA, l.portA) === subject ||
524 ForceSvgComponent.extractNodeName(l.epB, l.portB) === subject));
Sean Condonee545762019-03-09 10:43:58 +0000525 if (linkIdx >= 0) {
526 this.regionData.links.splice(linkIdx, 1);
527 this.log.debug('Link ', linkIdx, 'removed on attempt', i);
528 }
Sean Condon50855cf2018-12-23 15:37:42 +0000529 }
530 }
531
532 /**
533 * When traffic monitoring is turned on (A key) highlights will be sent back
534 * from the WebSocket through the Traffic Service
535 * @param devices - an array of device highlights
536 * @param hosts - an array of host highlights
537 * @param links - an array of link highlights
538 */
Sean Condon64ea7d22019-04-12 19:39:13 +0100539 handleHighlights(devices: Device[], hosts: Host[], links: LinkHighlight[], fadeMs: number = 0): void {
Sean Condon50855cf2018-12-23 15:37:42 +0000540
541 if (devices.length > 0) {
542 this.log.debug(devices.length, 'Devices highlighted');
543 devices.forEach((dh) => {
544 const deviceComponent: DeviceNodeSvgComponent = this.devices.find((d) => d.device.id === dh.id );
545 if (deviceComponent) {
546 deviceComponent.ngOnChanges(
547 {'deviceHighlight': new SimpleChange(<Device>{}, dh, true)}
548 );
549 this.log.debug('Highlighting device', deviceComponent.device.id);
550 } else {
551 this.log.warn('Device component not found', dh.id);
552 }
553 });
554 }
555 if (hosts.length > 0) {
556 this.log.debug(hosts.length, 'Hosts highlighted');
557 hosts.forEach((hh) => {
558 const hostComponent: HostNodeSvgComponent = this.hosts.find((h) => h.host.id === hh.id );
559 if (hostComponent) {
560 hostComponent.ngOnChanges(
561 {'hostHighlight': new SimpleChange(<Host>{}, hh, true)}
562 );
563 this.log.debug('Highlighting host', hostComponent.host.id);
564 }
565 });
566 }
567 if (links.length > 0) {
568 this.log.debug(links.length, 'Links highlighted');
569 links.forEach((lh) => {
Sean Condon64ea7d22019-04-12 19:39:13 +0100570 const linkComponent: LinkSvgComponent =
571 this.links.find((l) => l.link.id === Link.linkIdFromShowHighlights(lh.id) );
Sean Condon5f7d3bc2019-04-15 11:18:34 +0100572 if (linkComponent) { // A link might not be present if hosts viewing is switched off
Sean Condon64ea7d22019-04-12 19:39:13 +0100573 if (fadeMs > 0) {
574 lh.fadems = fadeMs;
575 }
Sean Condon50855cf2018-12-23 15:37:42 +0000576 linkComponent.ngOnChanges(
577 {'linkHighlight': new SimpleChange(<LinkHighlight>{}, lh, true)}
578 );
Sean Condon50855cf2018-12-23 15:37:42 +0000579 }
580 });
581 }
582 }
Sean Condon71910542019-02-16 18:16:42 +0000583
584 /**
585 * As nodes are dragged around the graph, their new location should be sent
586 * back to server
587 * @param klass The class of node e.g. 'host' or 'device'
588 * @param id - the ID of the node
589 * @param newLocation - the new Location of the node
590 */
591 nodeMoved(klass: string, id: string, newLocation: MetaUi) {
Sean Condon28884332019-03-21 14:07:00 +0000592 this.wss.sendEvent('updateMeta2', <UpdateMeta>{
Sean Condon71910542019-02-16 18:16:42 +0000593 id: id,
594 class: klass,
595 memento: newLocation
596 });
597 this.log.debug(klass, id, 'has been moved to', newLocation);
598 }
Sean Condon1ae15802019-03-02 09:07:18 +0000599
Sean Condon9de21352019-04-06 19:22:27 +0100600 /**
601 * If any nodes with fixed positions had been dragged out of place
602 * then put back where they belong
603 * If there are some devices selected reset only these
604 */
605 resetNodeLocations(): number {
606 let numbernodes = 0;
607 if (this.selectedNodes.length > 0) {
608 this.devices
609 .filter((d) => this.selectedNodes.some((s) => s.id === d.device.id))
610 .forEach((dev) => {
611 Node.resetNodeLocation(<Node>dev.device);
612 numbernodes++;
613 });
614 this.hosts
615 .filter((h) => this.selectedNodes.some((s) => s.id === h.host.id))
616 .forEach((h) => {
617 Host.resetNodeLocation(<Host>h.host);
618 numbernodes++;
619 });
620 } else {
621 this.devices.forEach((dev) => {
622 Node.resetNodeLocation(<Node>dev.device);
623 numbernodes++;
624 });
625 this.hosts.forEach((h) => {
626 Host.resetNodeLocation(<Host>h.host);
627 numbernodes++;
628 });
629 }
630 this.graph.reinitSimulation();
631 return numbernodes;
Sean Condon1ae15802019-03-02 09:07:18 +0000632 }
Sean Condon9de21352019-04-06 19:22:27 +0100633
634 /**
635 * Toggle floating nodes between unpinned and frozen
636 * There may be frozen and unpinned in the selection
637 *
638 * If there are nodes selected toggle only these
639 */
640 unpinOrFreezeNodes(freeze: boolean): number {
641 let numbernodes = 0;
642 if (this.selectedNodes.length > 0) {
643 this.devices
644 .filter((d) => this.selectedNodes.some((s) => s.id === d.device.id))
645 .forEach((d) => {
646 Node.unpinOrFreezeNode(<Node>d.device, freeze);
647 numbernodes++;
648 });
649 this.hosts
650 .filter((h) => this.selectedNodes.some((s) => s.id === h.host.id))
651 .forEach((h) => {
652 Node.unpinOrFreezeNode(<Node>h.host, freeze);
653 numbernodes++;
654 });
655 } else {
656 this.devices.forEach((d) => {
657 Node.unpinOrFreezeNode(<Node>d.device, freeze);
658 numbernodes++;
659 });
660 this.hosts.forEach((h) => {
661 Node.unpinOrFreezeNode(<Node>h.host, freeze);
662 numbernodes++;
663 });
664 }
665 this.graph.reinitSimulation();
666 return numbernodes;
667 }
668
Sean Condonf4f54a12018-10-10 23:25:46 +0100669}
Sean Condon0c577f62018-11-18 22:40:05 +0000670