blob: 234f1e9cc132fb809dae3020dfcff86aa3f60c10 [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,
Sean Condon590b34b2019-12-04 18:44:37 +000023 OnChanges, OnDestroy,
Sean Condon0c577f62018-11-18 22:40:05 +000024 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 Condon3dd062f2020-04-14 09:25:00 +010031import {LocMeta, LogService, MetaUi, WebSocketService, ZoomUtils} from 'org_onosproject_onos/web/gui2-fw-lib/public_api';
Sean Condon1ae15802019-03-02 09:07:18 +000032import {
Sean Condon590b34b2019-12-04 18:44:37 +000033 Badge,
34 Device, DeviceHighlight,
Sean Condon5f7d3bc2019-04-15 11:18:34 +010035 DeviceProps,
Sean Condon0c577f62018-11-18 22:40:05 +000036 ForceDirectedGraph,
Sean Condon590b34b2019-12-04 18:44:37 +000037 Host, HostHighlight,
Sean Condon50855cf2018-12-23 15:37:42 +000038 HostLabelToggle,
Sean Condon0c577f62018-11-18 22:40:05 +000039 LabelToggle,
40 LayerType,
Sean Condon50855cf2018-12-23 15:37:42 +000041 Link,
42 LinkHighlight,
Sean Condon1ae15802019-03-02 09:07:18 +000043 Location,
Sean Condon50855cf2018-12-23 15:37:42 +000044 ModelEventMemo,
45 ModelEventType,
Sean Condon28884332019-03-21 14:07:00 +000046 Node,
Sean Condond88f3662019-04-03 16:35:30 +010047 Options,
Sean Condon0c577f62018-11-18 22:40:05 +000048 Region,
49 RegionLink,
Sean Condon50855cf2018-12-23 15:37:42 +000050 SubRegion,
51 UiElement
Sean Condon0c577f62018-11-18 22:40:05 +000052} from './models';
Sean Condonee545762019-03-09 10:43:58 +000053import {LocationType} from '../backgroundsvg/backgroundsvg.component';
Sean Condonff85fbe2019-03-16 14:28:46 +000054import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
Sean Condon5f7d3bc2019-04-15 11:18:34 +010055import {HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
56import {LinkSvgComponent} from './visuals/linksvg/linksvg.component';
Sean Condond88f3662019-04-03 16:35:30 +010057import {SelectedEvent} from './visuals/nodevisual';
Sean Condon71910542019-02-16 18:16:42 +000058
59interface UpdateMeta {
60 id: string;
61 class: string;
62 memento: MetaUi;
63}
Sean Condonaa4366d2018-11-02 14:29:01 +000064
Sean Condon28884332019-03-21 14:07:00 +000065const SVGCANVAS = <Options>{
66 width: 1000,
67 height: 1000
68};
69
70interface ChangeSummary {
71 numChanges: number;
72 locationChanged: boolean;
73}
74
Sean Condonf4f54a12018-10-10 23:25:46 +010075/**
76 * ONOS GUI -- Topology Forces Graph Layer View.
Sean Condon0c577f62018-11-18 22:40:05 +000077 *
78 * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
79 * This drives the whole Force graph
Sean Condonf4f54a12018-10-10 23:25:46 +010080 */
81@Component({
82 selector: '[onos-forcesvg]',
83 templateUrl: './forcesvg.component.html',
Sean Condon0c577f62018-11-18 22:40:05 +000084 styleUrls: ['./forcesvg.component.css'],
85 changeDetection: ChangeDetectionStrategy.OnPush,
Sean Condonf4f54a12018-10-10 23:25:46 +010086})
Sean Condon590b34b2019-12-04 18:44:37 +000087export class ForceSvgComponent implements OnInit, OnDestroy, OnChanges {
Sean Condonff85fbe2019-03-16 14:28:46 +000088 @Input() deviceLabelToggle: LabelToggle.Enum = LabelToggle.Enum.NONE;
89 @Input() hostLabelToggle: HostLabelToggle.Enum = HostLabelToggle.Enum.NONE;
Sean Condonb2c483c2019-01-16 20:28:55 +000090 @Input() showHosts: boolean = false;
Sean Condon590b34b2019-12-04 18:44:37 +000091 @Input() showAlarms: boolean = false;
Sean Condonb2c483c2019-01-16 20:28:55 +000092 @Input() highlightPorts: boolean = true;
93 @Input() onosInstMastership: string = '';
94 @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
95 @Input() selectedLink: RegionLink = null;
Sean Condon1ae15802019-03-02 09:07:18 +000096 @Input() scale: number = 1;
Sean Condon0c577f62018-11-18 22:40:05 +000097 @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
Sean Condonb2c483c2019-01-16 20:28:55 +000098 @Output() linkSelected = new EventEmitter<RegionLink>();
Sean Condond88f3662019-04-03 16:35:30 +010099 @Output() selectedNodeEvent = new EventEmitter<UiElement[]>();
Sean Condonff85fbe2019-03-16 14:28:46 +0000100 public graph: ForceDirectedGraph;
Sean Condond88f3662019-04-03 16:35:30 +0100101 private selectedNodes: UiElement[] = [];
Sean Condon590b34b2019-12-04 18:44:37 +0000102 viewInitialized: boolean = false;
Sean Condon00e56d02020-02-28 09:50:04 +0000103 linksHighlighted: Map<string, LinkHighlight>;
Sean Condonf4f54a12018-10-10 23:25:46 +0100104
Sean Condon021f0fa2018-12-06 23:31:11 -0800105 // References to the children of this component - these are created in the
106 // template view with the *ngFor and we get them by a query here
Sean Condon0c577f62018-11-18 22:40:05 +0000107 @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
Sean Condon021f0fa2018-12-06 23:31:11 -0800108 @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
Sean Condon50855cf2018-12-23 15:37:42 +0000109 @ViewChildren(LinkSvgComponent) links: QueryList<LinkSvgComponent>;
Sean Condonf4f54a12018-10-10 23:25:46 +0100110
Sean Condon0c577f62018-11-18 22:40:05 +0000111 constructor(
112 protected log: LogService,
Sean Condon71910542019-02-16 18:16:42 +0000113 private ref: ChangeDetectorRef,
114 protected wss: WebSocketService
Sean Condon0c577f62018-11-18 22:40:05 +0000115 ) {
116 this.selectedLink = null;
117 this.log.debug('ForceSvgComponent constructed');
Sean Condon00e56d02020-02-28 09:50:04 +0000118 this.linksHighlighted = new Map<string, LinkHighlight>();
Sean Condon0c577f62018-11-18 22:40:05 +0000119 }
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
pierventre57f7bbb2021-10-20 17:37:52 +0200126 * @param portStr The port name
Sean Condon021f0fa2018-12-06 23:31:11 -0800127 */
Sean Condonb77768e2019-05-04 20:23:42 +0100128 static extractNodeName(endPtStr: string, portStr: string): string {
129 if (portStr === undefined || endPtStr === undefined) {
Sean Condon021f0fa2018-12-06 23:31:11 -0800130 return endPtStr;
Sean Condonb77768e2019-05-04 20:23:42 +0100131 } else if (endPtStr.includes('/')) {
132 return endPtStr.substr(0, endPtStr.length - portStr.length - 1);
Sean Condon021f0fa2018-12-06 23:31:11 -0800133 }
Sean Condonb77768e2019-05-04 20:23:42 +0100134 return endPtStr;
Sean Condon021f0fa2018-12-06 23:31:11 -0800135 }
136
Sean Condonee545762019-03-09 10:43:58 +0000137 /**
138 * Recursive method to compare 2 objects attribute by attribute and update
139 * the first where a change is detected
140 * @param existingNode 1st object
141 * @param updatedNode 2nd object
142 */
Sean Condon28884332019-03-21 14:07:00 +0000143 private static updateObject(existingNode: Object, updatedNode: Object): ChangeSummary {
144 const changed = <ChangeSummary>{numChanges: 0, locationChanged: false};
Sean Condonee545762019-03-09 10:43:58 +0000145 for (const key of Object.keys(updatedNode)) {
146 const o = updatedNode[key];
Sean Condon28884332019-03-21 14:07:00 +0000147 if (['id', 'x', 'y', 'fx', 'fy', 'vx', 'vy', 'index'].some(k => k === key)) {
Sean Condonee545762019-03-09 10:43:58 +0000148 continue;
149 } else if (o && typeof o === 'object' && o.constructor === Object) {
Sean Condon28884332019-03-21 14:07:00 +0000150 const subChanged = ForceSvgComponent.updateObject(existingNode[key], updatedNode[key]);
151 changed.numChanges += subChanged.numChanges;
152 changed.locationChanged = subChanged.locationChanged ? true : changed.locationChanged;
153 } else if (existingNode === undefined) {
154 // Copy the whole object
155 existingNode = updatedNode;
156 changed.locationChanged = true;
157 changed.numChanges++;
Sean Condonee545762019-03-09 10:43:58 +0000158 } else if (existingNode[key] !== updatedNode[key]) {
Sean Condon28884332019-03-21 14:07:00 +0000159 if (['locType', 'latOrY', 'longOrX', 'latitude', 'longitude', 'gridX', 'gridY'].some(k => k === key)) {
160 changed.locationChanged = true;
161 }
162 changed.numChanges++;
Sean Condonee545762019-03-09 10:43:58 +0000163 existingNode[key] = updatedNode[key];
164 }
165 }
166 return changed;
167 }
168
Sean Condon021f0fa2018-12-06 23:31:11 -0800169 @HostListener('window:resize', ['$event'])
170 onResize(event) {
Sean Condon28884332019-03-21 14:07:00 +0000171 this.graph.restartSimulation();
172 this.log.debug('Simulation restart after resize', event);
Sean Condon021f0fa2018-12-06 23:31:11 -0800173 }
174
175 /**
Sean Condon0c577f62018-11-18 22:40:05 +0000176 * After the component is initialized create the Force simulation
Sean Condon021f0fa2018-12-06 23:31:11 -0800177 * The list of devices, hosts and links will not have been receieved back
178 * from the WebSocket yet as this time - they will be updated later through
179 * ngOnChanges()
Sean Condon0c577f62018-11-18 22:40:05 +0000180 */
181 ngOnInit() {
182 // Receiving an initialized simulated graph from our custom d3 service
Sean Condon28884332019-03-21 14:07:00 +0000183 this.graph = new ForceDirectedGraph(SVGCANVAS, this.log);
Sean Condon0c577f62018-11-18 22:40:05 +0000184
185 /** Binding change detection check on each tick
Sean Condon021f0fa2018-12-06 23:31:11 -0800186 * This along with an onPush change detection strategy should enforce
187 * checking only when relevant! This improves scripting computation
188 * duration in a couple of tests I've made, consistently. Also, it makes
189 * sense to avoid unnecessary checks when we are dealing only with
190 * simulations data binding.
Sean Condon0c577f62018-11-18 22:40:05 +0000191 */
192 this.graph.ticker.subscribe((simulation) => {
Sean Condon28884332019-03-21 14:07:00 +0000193 // this.log.debug("Force simulation has ticked. Alpha",
194 // Math.round(simulation.alpha() * 1000) / 1000);
Sean Condon0c577f62018-11-18 22:40:05 +0000195 this.ref.markForCheck();
196 });
Sean Condon28884332019-03-21 14:07:00 +0000197
Sean Condon0c577f62018-11-18 22:40:05 +0000198 this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
199
Sean Condon0c577f62018-11-18 22:40:05 +0000200 }
201
202 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800203 * When any one of the inputs get changed by a containing component, this
204 * gets called automatically. In addition this is called manually by
205 * topology.service when a response is received from the WebSocket from the
206 * server
Sean Condon0c577f62018-11-18 22:40:05 +0000207 *
208 * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
209 * The Links are added to the Link list of the simulation.
210 * Before they are added the Links are associated with Nodes based on their endPt
211 *
212 * @param changes - a list of changed @Input(s)
213 */
214 ngOnChanges(changes: SimpleChanges) {
215 if (changes['regionData']) {
216 const devices: Device[] =
217 changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
218 const hosts: Host[] =
219 changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
220 const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
221 this.graph.nodes = [];
222 if (devices) {
223 this.graph.nodes = devices;
224 }
225 if (hosts) {
226 this.graph.nodes = this.graph.nodes.concat(hosts);
227 }
228 if (subRegions) {
229 this.graph.nodes = this.graph.nodes.concat(subRegions);
230 }
231
Sean Condon28884332019-03-21 14:07:00 +0000232 this.graph.nodes.forEach((n) => this.fixPosition(n));
Sean Condon71910542019-02-16 18:16:42 +0000233
Sean Condon0c577f62018-11-18 22:40:05 +0000234 // Associate the endpoints of each link with a real node
235 this.graph.links = [];
236 for (const linkIdx of Object.keys(this.regionData.links)) {
Sean Condonb77768e2019-05-04 20:23:42 +0100237 const link = this.regionData.links[linkIdx];
238 const epA = ForceSvgComponent.extractNodeName(link.epA, link.portA);
239 if (!this.graph.nodes.find((node) => node.id === epA)) {
240 this.log.error('ngOnChange Could not find endpoint A', epA, 'for', link);
241 continue;
242 }
243 const epB = ForceSvgComponent.extractNodeName(
244 link.epB, link.portB);
245 if (!this.graph.nodes.find((node) => node.id === epB)) {
246 this.log.error('ngOnChange Could not find endpoint B', epB, 'for', link);
247 continue;
248 }
Sean Condon0c577f62018-11-18 22:40:05 +0000249 this.regionData.links[linkIdx].source =
250 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800251 node.id === epA);
Sean Condon0c577f62018-11-18 22:40:05 +0000252 this.regionData.links[linkIdx].target =
253 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800254 node.id === epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000255 this.regionData.links[linkIdx].index = Number(linkIdx);
256 }
257
258 this.graph.links = this.regionData.links;
Sean Condon28884332019-03-21 14:07:00 +0000259 if (this.graph.nodes.length > 0) {
260 this.graph.reinitSimulation();
261 }
Sean Condon0c577f62018-11-18 22:40:05 +0000262 this.log.debug('ForceSvgComponent input changed',
263 this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
Sean Condon590b34b2019-12-04 18:44:37 +0000264 if (!this.viewInitialized) {
265 this.viewInitialized = true;
266 if (this.showAlarms) {
267 this.wss.sendEvent('alarmTopovDisplayStart', {});
268 }
269 }
Sean Condon0c577f62018-11-18 22:40:05 +0000270 }
Sean Condon590b34b2019-12-04 18:44:37 +0000271
272 if (changes['showAlarms'] && this.viewInitialized) {
273 if (this.showAlarms) {
274 this.wss.sendEvent('alarmTopovDisplayStart', {});
275 } else {
276 this.wss.sendEvent('alarmTopovDisplayStop', {});
277 this.cancelAllDeviceHighlightsNow();
278 }
279 }
280 }
281
282 ngOnDestroy(): void {
283 if (this.showAlarms) {
284 this.wss.sendEvent('alarmTopovDisplayStop', {});
285 this.cancelAllDeviceHighlightsNow();
286 }
287 this.viewInitialized = false;
Sean Condon28884332019-03-21 14:07:00 +0000288 }
Sean Condon021f0fa2018-12-06 23:31:11 -0800289
Sean Condon28884332019-03-21 14:07:00 +0000290 /**
Sean Condon058804c2019-04-16 09:41:52 +0100291 * If instance has a value then mute colors of devices not connected to it
292 * Otherwise if instance does not have a value unmute all
293 * @param instanceName name of the selected instance
294 */
295 changeInstSelection(instanceName: string) {
296 this.log.debug('Mastership changed', instanceName);
297 this.devices.filter((d) => d.device.master !== instanceName)
298 .forEach((d) => {
299 const isMuted = Boolean(instanceName);
300 d.ngOnChanges({'colorMuted': new SimpleChange(!isMuted, isMuted, true)});
301 }
302 );
303 }
304
305 /**
Sean Condon28884332019-03-21 14:07:00 +0000306 * If a node has a fixed location then assign it to fx and fy so
307 * that it doesn't get affected by forces
308 * @param graphNode The node whose location should be processed
309 */
310 private fixPosition(graphNode: Node): void {
311 const loc: Location = <Location>graphNode['location'];
312 const props: DeviceProps = <DeviceProps>graphNode['props'];
313 const metaUi = <MetaUi>graphNode['metaUi'];
314 if (loc && loc.locType === LocationType.GEO) {
315 const position: MetaUi =
316 ZoomUtils.convertGeoToCanvas(
317 <LocMeta>{lng: loc.longOrX, lat: loc.latOrY});
318 graphNode.fx = position.x;
319 graphNode.fy = position.y;
320 this.log.debug('Found node', graphNode.id, 'with', loc.locType);
321 } else if (loc && loc.locType === LocationType.GRID) {
322 graphNode.fx = loc.longOrX;
323 graphNode.fy = loc.latOrY;
324 this.log.debug('Found node', graphNode.id, 'with', loc.locType);
325 } else if (props && props.locType === LocationType.NONE && metaUi) {
326 graphNode.fx = metaUi.x;
327 graphNode.fy = metaUi.y;
328 this.log.debug('Found node', graphNode.id, 'with locType=none and metaUi');
329 } else {
330 graphNode.fx = null;
331 graphNode.fy = null;
332 }
Sean Condon0c577f62018-11-18 22:40:05 +0000333 }
334
335 /**
336 * Get the index of LayerType so it can drive the visibility of nodes and
337 * hosts on layers
338 */
339 visibleLayerIdx(): number {
340 const layerKeys: string[] = Object.keys(LayerType);
341 for (const idx in layerKeys) {
342 if (LayerType[layerKeys[idx]] === this.visibleLayer) {
343 return Number(idx);
344 }
345 }
346 return -1;
347 }
348
349 selectLink(link: RegionLink): void {
350 this.selectedLink = link;
351 this.linkSelected.emit(link);
352 }
353
Sean Condon021f0fa2018-12-06 23:31:11 -0800354 /**
Sean Condond88f3662019-04-03 16:35:30 +0100355 * Iterate through all hosts and devices and links to deselect the previously selected
Sean Condon021f0fa2018-12-06 23:31:11 -0800356 * node. The emit an event to the parent that lets it know the selection has
357 * changed.
Sean Condond88f3662019-04-03 16:35:30 +0100358 *
359 * This function collates all of the nodes that have been selected and passes
360 * a collection of nodes up to the topology component
361 *
Sean Condon021f0fa2018-12-06 23:31:11 -0800362 * @param selectedNode the newly selected node
363 */
Sean Condond88f3662019-04-03 16:35:30 +0100364 updateSelected(selectedNode: SelectedEvent): void {
365 this.log.debug('Node or link ',
Sean Condon64ea7d22019-04-12 19:39:13 +0100366 selectedNode.uiElement ? selectedNode.uiElement.id : '--',
Sean Condond88f3662019-04-03 16:35:30 +0100367 selectedNode.deselecting ? 'deselected' : 'selected',
368 selectedNode.isShift ? 'Multiple' : '');
Sean Condon021f0fa2018-12-06 23:31:11 -0800369
Sean Condond88f3662019-04-03 16:35:30 +0100370 if (selectedNode.isShift && selectedNode.deselecting) {
371 const idx = this.selectedNodes.findIndex((n) =>
372 n.id === selectedNode.uiElement.id
373 );
374 this.selectedNodes.splice(idx, 1);
375 this.log.debug('Removed node', idx);
376
377 } else if (selectedNode.isShift) {
378 this.selectedNodes.push(selectedNode.uiElement);
379
380 } else if (selectedNode.deselecting) {
Sean Condon64ea7d22019-04-12 19:39:13 +0100381 this.devices
382 .forEach((d) => d.deselect());
383 this.hosts
384 .forEach((h) => h.deselect());
385 this.links
386 .forEach((l) => l.deselect());
Sean Condond88f3662019-04-03 16:35:30 +0100387 this.selectedNodes = [];
388
389 } else {
390 const selNodeId = selectedNode.uiElement.id;
391 // Otherwise if shift was not pressed deselect previous
392 this.devices
393 .filter((d) => d.device.id !== selNodeId)
394 .forEach((d) => d.deselect());
395 this.hosts
396 .filter((h) => h.host.id !== selNodeId)
397 .forEach((h) => h.deselect());
398
399 this.links
400 .filter((l) => l.link.id !== selNodeId)
401 .forEach((l) => l.deselect());
402
403 this.selectedNodes = [selectedNode.uiElement];
404 }
Sean Condon91481822019-01-01 13:56:14 +0000405 // Push the changes back up to parent (Topology Component)
Sean Condond88f3662019-04-03 16:35:30 +0100406 this.selectedNodeEvent.emit(this.selectedNodes);
Sean Condon0c577f62018-11-18 22:40:05 +0000407 }
408
Sean Condon021f0fa2018-12-06 23:31:11 -0800409 /**
410 * We want to filter links to show only those not related to hosts if the
Sean Condon50855cf2018-12-23 15:37:42 +0000411 * 'showHosts' flag has been switched off. If 'showHosts' is true, then
Sean Condon021f0fa2018-12-06 23:31:11 -0800412 * display all links.
413 */
Sean Condon50855cf2018-12-23 15:37:42 +0000414 filteredLinks(): Link[] {
Sean Condon021f0fa2018-12-06 23:31:11 -0800415 return this.regionData.links.filter((h) =>
416 this.showHosts ||
417 ((<Host>h.source).nodeType !== 'host' &&
418 (<Host>h.target).nodeType !== 'host'));
Sean Condon0c577f62018-11-18 22:40:05 +0000419 }
Sean Condon50855cf2018-12-23 15:37:42 +0000420
421 /**
422 * When changes happen in the model, then model events are sent up through the
423 * Web Socket
424 * @param type - the type of the change
425 * @param memo - a qualifier on the type
426 * @param subject - the item that the update is for
427 * @param data - the new definition of the item
428 */
429 handleModelEvent(type: ModelEventType, memo: ModelEventMemo, subject: string, data: UiElement): void {
430 switch (type) {
431 case ModelEventType.DEVICE_ADDED_OR_UPDATED:
432 if (memo === ModelEventMemo.ADDED) {
Sean Condon28884332019-03-21 14:07:00 +0000433 this.fixPosition(<Device>data);
Sean Condonee545762019-03-09 10:43:58 +0000434 this.graph.nodes.push(<Device>data);
Sean Condonee5d4b92019-03-11 19:57:34 +0000435 this.regionData.devices[this.visibleLayerIdx()].push(<Device>data);
Sean Condonee545762019-03-09 10:43:58 +0000436 this.log.debug('Device added', (<Device>data).id);
Sean Condon50855cf2018-12-23 15:37:42 +0000437 } else if (memo === ModelEventMemo.UPDATED) {
438 const oldDevice: Device =
439 this.regionData.devices[this.visibleLayerIdx()]
440 .find((d) => d.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000441 const changes = ForceSvgComponent.updateObject(oldDevice, <Device>data);
Sean Condon28884332019-03-21 14:07:00 +0000442 if (changes.numChanges > 0) {
Sean Condonee545762019-03-09 10:43:58 +0000443 this.log.debug('Device ', oldDevice.id, memo, ' - ', changes, 'changes');
Sean Condon28884332019-03-21 14:07:00 +0000444 if (changes.locationChanged) {
445 this.fixPosition(oldDevice);
446 }
Sean Condon058804c2019-04-16 09:41:52 +0100447 const svgDevice: DeviceNodeSvgComponent =
448 this.devices.find((svgdevice) => svgdevice.device.id === subject);
449 svgDevice.ngOnChanges({'device':
450 new SimpleChange(<Device>{}, oldDevice, true)
451 });
Sean Condonee545762019-03-09 10:43:58 +0000452 }
Sean Condon50855cf2018-12-23 15:37:42 +0000453 } else {
454 this.log.warn('Device ', memo, ' - not yet implemented', data);
455 }
Sean Condon50855cf2018-12-23 15:37:42 +0000456 break;
457 case ModelEventType.HOST_ADDED_OR_UPDATED:
458 if (memo === ModelEventMemo.ADDED) {
Sean Condon28884332019-03-21 14:07:00 +0000459 this.fixPosition(<Host>data);
Sean Condonee545762019-03-09 10:43:58 +0000460 this.graph.nodes.push(<Host>data);
Sean Condon28884332019-03-21 14:07:00 +0000461 this.regionData.hosts[this.visibleLayerIdx()].push(<Host>data);
Sean Condonee545762019-03-09 10:43:58 +0000462 this.log.debug('Host added', (<Host>data).id);
Sean Condon50855cf2018-12-23 15:37:42 +0000463 } else if (memo === ModelEventMemo.UPDATED) {
464 const oldHost: Host = this.regionData.hosts[this.visibleLayerIdx()]
465 .find((h) => h.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000466 const changes = ForceSvgComponent.updateObject(oldHost, <Host>data);
Sean Condon28884332019-03-21 14:07:00 +0000467 if (changes.numChanges > 0) {
Sean Condonee545762019-03-09 10:43:58 +0000468 this.log.debug('Host ', oldHost.id, memo, ' - ', changes, 'changes');
Sean Condon28884332019-03-21 14:07:00 +0000469 if (changes.locationChanged) {
470 this.fixPosition(oldHost);
471 }
Sean Condonee545762019-03-09 10:43:58 +0000472 }
Sean Condon50855cf2018-12-23 15:37:42 +0000473 } else {
474 this.log.warn('Host change', memo, ' - unexpected');
475 }
476 break;
477 case ModelEventType.DEVICE_REMOVED:
478 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
479 const removeIdx: number =
480 this.regionData.devices[this.visibleLayerIdx()]
481 .findIndex((d) => d.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000482 this.regionData.devices[this.visibleLayerIdx()].splice(removeIdx, 1);
483 this.removeRelatedLinks(subject);
484 this.log.debug('Device ', subject, 'removed. Links', this.regionData.links);
Sean Condon50855cf2018-12-23 15:37:42 +0000485 } else {
486 this.log.warn('Device removed - unexpected memo', memo);
487 }
488 break;
489 case ModelEventType.HOST_REMOVED:
490 if (memo === ModelEventMemo.REMOVED || memo === undefined) {
491 const removeIdx: number =
492 this.regionData.hosts[this.visibleLayerIdx()]
493 .findIndex((h) => h.id === subject);
Sean Condonee545762019-03-09 10:43:58 +0000494 this.regionData.hosts[this.visibleLayerIdx()].splice(removeIdx, 1);
495 this.removeRelatedLinks(subject);
Sean Condon5f7d3bc2019-04-15 11:18:34 +0100496 this.log.debug('Host ', subject, 'removed');
Sean Condon50855cf2018-12-23 15:37:42 +0000497 } else {
498 this.log.warn('Host removed - unexpected memo', memo);
499 }
500 break;
501 case ModelEventType.LINK_ADDED_OR_UPDATED:
Sean Condonee545762019-03-09 10:43:58 +0000502 if (memo === ModelEventMemo.ADDED &&
503 this.regionData.links.findIndex((l) => l.id === subject) === -1) {
Sean Condonb77768e2019-05-04 20:23:42 +0100504 const newLink = <RegionLink>data;
505
506
Sean Condonee545762019-03-09 10:43:58 +0000507 const epA = ForceSvgComponent.extractNodeName(
Sean Condonb77768e2019-05-04 20:23:42 +0100508 newLink.epA, newLink.portA);
509 if (!this.graph.nodes.find((node) => node.id === epA)) {
510 this.log.error('Could not find endpoint A', epA, 'of', newLink);
511 break;
512 }
Sean Condonee545762019-03-09 10:43:58 +0000513 const epB = ForceSvgComponent.extractNodeName(
Sean Condonb77768e2019-05-04 20:23:42 +0100514 newLink.epB, newLink.portB);
515 if (!this.graph.nodes.find((node) => node.id === epB)) {
516 this.log.error('Could not find endpoint B', epB, 'of link', newLink);
517 break;
518 }
519
520 const listLen = this.regionData.links.push(<RegionLink>data);
521 this.regionData.links[listLen - 1].source =
522 this.graph.nodes.find((node) => node.id === epA);
Sean Condonee545762019-03-09 10:43:58 +0000523 this.regionData.links[listLen - 1].target =
Sean Condonb77768e2019-05-04 20:23:42 +0100524 this.graph.nodes.find((node) => node.id === epB);
Sean Condonee545762019-03-09 10:43:58 +0000525 this.log.debug('Link added', subject);
526 } else if (memo === ModelEventMemo.UPDATED) {
527 const oldLink = this.regionData.links.find((l) => l.id === subject);
528 const changes = ForceSvgComponent.updateObject(oldLink, <RegionLink>data);
529 this.log.debug('Link ', subject, '. Updated', changes, 'items');
530 } else {
Sean Condon5f7d3bc2019-04-15 11:18:34 +0100531 this.log.warn('Link event ignored', subject, data);
532 }
533 break;
534 case ModelEventType.LINK_REMOVED:
535 if (memo === ModelEventMemo.REMOVED) {
536 const removeIdx = this.regionData.links.findIndex((l) => l.id === subject);
537 this.regionData.links.splice(removeIdx, 1);
538 this.log.debug('Link ', subject, 'removed');
Sean Condonee545762019-03-09 10:43:58 +0000539 }
Sean Condon50855cf2018-12-23 15:37:42 +0000540 break;
541 default:
Sean Condon5f7d3bc2019-04-15 11:18:34 +0100542 this.log.error('Unexpected model event', type, 'for', subject, 'Data', data);
Sean Condon50855cf2018-12-23 15:37:42 +0000543 }
Sean Condon28884332019-03-21 14:07:00 +0000544 this.graph.links = this.regionData.links;
545 this.graph.reinitSimulation();
Sean Condon50855cf2018-12-23 15:37:42 +0000546 }
547
Sean Condonee545762019-03-09 10:43:58 +0000548 private removeRelatedLinks(subject: string) {
549 const len = this.regionData.links.length;
550 for (let i = 0; i < len; i++) {
551 const linkIdx = this.regionData.links.findIndex((l) =>
Sean Condonb77768e2019-05-04 20:23:42 +0100552 (ForceSvgComponent.extractNodeName(l.epA, l.portA) === subject ||
553 ForceSvgComponent.extractNodeName(l.epB, l.portB) === subject));
Sean Condonee545762019-03-09 10:43:58 +0000554 if (linkIdx >= 0) {
555 this.regionData.links.splice(linkIdx, 1);
556 this.log.debug('Link ', linkIdx, 'removed on attempt', i);
557 }
Sean Condon50855cf2018-12-23 15:37:42 +0000558 }
559 }
560
561 /**
562 * When traffic monitoring is turned on (A key) highlights will be sent back
563 * from the WebSocket through the Traffic Service
Sean Condon4e55c802019-12-03 22:13:34 +0000564 * Also handles Intent highlights in case one is selected
Sean Condon50855cf2018-12-23 15:37:42 +0000565 * @param devices - an array of device highlights
566 * @param hosts - an array of host highlights
567 * @param links - an array of link highlights
568 */
Sean Condon590b34b2019-12-04 18:44:37 +0000569 handleHighlights(devices: DeviceHighlight[], hosts: HostHighlight[], links: LinkHighlight[], fadeMs: number = 0): void {
Sean Condon50855cf2018-12-23 15:37:42 +0000570
571 if (devices.length > 0) {
572 this.log.debug(devices.length, 'Devices highlighted');
Sean Condon590b34b2019-12-04 18:44:37 +0000573 devices.forEach((dh: DeviceHighlight) => {
574 this.devices.forEach((d: DeviceNodeSvgComponent) => {
575 if (d.device.id === dh.id) {
576 d.badge = dh.badge;
577 this.ref.markForCheck(); // Forces ngOnChange in the DeviceSvgComponent
578 this.log.debug('Highlighting device', dh.id);
579 }
580 });
Sean Condon50855cf2018-12-23 15:37:42 +0000581 });
582 }
583 if (hosts.length > 0) {
584 this.log.debug(hosts.length, 'Hosts highlighted');
Sean Condon590b34b2019-12-04 18:44:37 +0000585 hosts.forEach((hh: HostHighlight) => {
586 this.hosts.forEach((h) => {
587 if (h.host.id === hh.id) {
588 h.badge = hh.badge;
589 this.ref.markForCheck(); // Forces ngOnChange in the HostSvgComponent
590 this.log.debug('Highlighting host', hh.id);
591 }
592 });
Sean Condon50855cf2018-12-23 15:37:42 +0000593 });
594 }
595 if (links.length > 0) {
596 this.log.debug(links.length, 'Links highlighted');
Sean Condon4e55c802019-12-03 22:13:34 +0000597 links.forEach((lh: LinkHighlight) => {
598 if (fadeMs > 0) {
599 lh.fadems = fadeMs;
Sean Condon50855cf2018-12-23 15:37:42 +0000600 }
Sean Condon00e56d02020-02-28 09:50:04 +0000601 this.linksHighlighted.set(Link.linkIdFromShowHighlights(lh.id), lh);
Sean Condon50855cf2018-12-23 15:37:42 +0000602 });
Sean Condon00e56d02020-02-28 09:50:04 +0000603 this.ref.detectChanges(); // Forces ngOnChange in the LinkSvgComponent
Sean Condon50855cf2018-12-23 15:37:42 +0000604 }
605 }
Sean Condon71910542019-02-16 18:16:42 +0000606
Sean Condon590b34b2019-12-04 18:44:37 +0000607 cancelAllHostHighlightsNow() {
608 this.hosts.forEach((host: HostNodeSvgComponent) => {
609 host.badge = undefined;
610 this.ref.markForCheck(); // Forces ngOnChange in the HostSvgComponent
611 });
612 }
613
614 cancelAllDeviceHighlightsNow() {
615 this.devices.forEach((device: DeviceNodeSvgComponent) => {
616 device.badge = undefined;
617 this.ref.markForCheck(); // Forces ngOnChange in the DeviceSvgComponent
618 });
619 }
620
Sean Condon4e55c802019-12-03 22:13:34 +0000621 cancelAllLinkHighlightsNow() {
622 this.links.forEach((link: LinkSvgComponent) => {
623 link.linkHighlight = <LinkHighlight>{};
Sean Condon590b34b2019-12-04 18:44:37 +0000624 this.ref.markForCheck(); // Forces ngOnChange in the LinkSvgComponent
Sean Condon4e55c802019-12-03 22:13:34 +0000625 });
626 }
627
Sean Condon71910542019-02-16 18:16:42 +0000628 /**
629 * As nodes are dragged around the graph, their new location should be sent
630 * back to server
631 * @param klass The class of node e.g. 'host' or 'device'
632 * @param id - the ID of the node
633 * @param newLocation - the new Location of the node
634 */
635 nodeMoved(klass: string, id: string, newLocation: MetaUi) {
Sean Condon28884332019-03-21 14:07:00 +0000636 this.wss.sendEvent('updateMeta2', <UpdateMeta>{
Sean Condon71910542019-02-16 18:16:42 +0000637 id: id,
638 class: klass,
639 memento: newLocation
640 });
641 this.log.debug(klass, id, 'has been moved to', newLocation);
642 }
Sean Condon1ae15802019-03-02 09:07:18 +0000643
Sean Condon9de21352019-04-06 19:22:27 +0100644 /**
645 * If any nodes with fixed positions had been dragged out of place
646 * then put back where they belong
647 * If there are some devices selected reset only these
648 */
649 resetNodeLocations(): number {
650 let numbernodes = 0;
651 if (this.selectedNodes.length > 0) {
652 this.devices
653 .filter((d) => this.selectedNodes.some((s) => s.id === d.device.id))
654 .forEach((dev) => {
655 Node.resetNodeLocation(<Node>dev.device);
656 numbernodes++;
657 });
658 this.hosts
659 .filter((h) => this.selectedNodes.some((s) => s.id === h.host.id))
660 .forEach((h) => {
661 Host.resetNodeLocation(<Host>h.host);
662 numbernodes++;
663 });
664 } else {
665 this.devices.forEach((dev) => {
666 Node.resetNodeLocation(<Node>dev.device);
667 numbernodes++;
668 });
669 this.hosts.forEach((h) => {
670 Host.resetNodeLocation(<Host>h.host);
671 numbernodes++;
672 });
673 }
674 this.graph.reinitSimulation();
675 return numbernodes;
Sean Condon1ae15802019-03-02 09:07:18 +0000676 }
Sean Condon9de21352019-04-06 19:22:27 +0100677
678 /**
679 * Toggle floating nodes between unpinned and frozen
680 * There may be frozen and unpinned in the selection
681 *
682 * If there are nodes selected toggle only these
683 */
684 unpinOrFreezeNodes(freeze: boolean): number {
685 let numbernodes = 0;
686 if (this.selectedNodes.length > 0) {
687 this.devices
688 .filter((d) => this.selectedNodes.some((s) => s.id === d.device.id))
689 .forEach((d) => {
690 Node.unpinOrFreezeNode(<Node>d.device, freeze);
691 numbernodes++;
692 });
693 this.hosts
694 .filter((h) => this.selectedNodes.some((s) => s.id === h.host.id))
695 .forEach((h) => {
696 Node.unpinOrFreezeNode(<Node>h.host, freeze);
697 numbernodes++;
698 });
699 } else {
700 this.devices.forEach((d) => {
701 Node.unpinOrFreezeNode(<Node>d.device, freeze);
702 numbernodes++;
703 });
704 this.hosts.forEach((h) => {
705 Node.unpinOrFreezeNode(<Node>h.host, freeze);
706 numbernodes++;
707 });
708 }
709 this.graph.reinitSimulation();
710 return numbernodes;
711 }
Sean Condonf4f54a12018-10-10 23:25:46 +0100712}
Sean Condon0c577f62018-11-18 22:40:05 +0000713