blob: 1d6ebf465bb7d32dd84525a50d7e3b6bf73e1732 [file] [log] [blame]
Sean Condonf4f54a12018-10-10 23:25:46 +01001/*
2 * Copyright 2018-present Open Networking Foundation
3 *
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,
27 SimpleChanges,
28 ViewChildren
Sean Condon0c577f62018-11-18 22:40:05 +000029} from '@angular/core';
30import {IconService, LogService} from 'gui2-fw-lib';
31import {
32 Device,
33 ForceDirectedGraph,
Sean Condon021f0fa2018-12-06 23:31:11 -080034 Host, HostLabelToggle,
Sean Condon0c577f62018-11-18 22:40:05 +000035 LabelToggle,
36 LayerType,
Sean Condon021f0fa2018-12-06 23:31:11 -080037 Node,
Sean Condon0c577f62018-11-18 22:40:05 +000038 Region,
39 RegionLink,
40 SubRegion
41} from './models';
Sean Condon021f0fa2018-12-06 23:31:11 -080042import {DeviceNodeSvgComponent, HostNodeSvgComponent} from './visuals';
Sean Condonaa4366d2018-11-02 14:29:01 +000043
Sean Condonf4f54a12018-10-10 23:25:46 +010044
45/**
46 * ONOS GUI -- Topology Forces Graph Layer View.
Sean Condon0c577f62018-11-18 22:40:05 +000047 *
48 * The regionData is set by Topology Service on WebSocket topo2CurrentRegion callback
49 * This drives the whole Force graph
Sean Condonf4f54a12018-10-10 23:25:46 +010050 */
51@Component({
52 selector: '[onos-forcesvg]',
53 templateUrl: './forcesvg.component.html',
Sean Condon0c577f62018-11-18 22:40:05 +000054 styleUrls: ['./forcesvg.component.css'],
55 changeDetection: ChangeDetectionStrategy.OnPush,
Sean Condonf4f54a12018-10-10 23:25:46 +010056})
Sean Condon0c577f62018-11-18 22:40:05 +000057export class ForceSvgComponent implements OnInit, OnChanges {
Sean Condonaa4366d2018-11-02 14:29:01 +000058 @Input() onosInstMastership: string = '';
Sean Condon0c577f62018-11-18 22:40:05 +000059 @Input() visibleLayer: LayerType = LayerType.LAYER_DEFAULT;
60 @Output() linkSelected = new EventEmitter<RegionLink>();
Sean Condon021f0fa2018-12-06 23:31:11 -080061 @Output() selectedNodeEvent = new EventEmitter<Node>();
Sean Condon0c577f62018-11-18 22:40:05 +000062 @Input() selectedLink: RegionLink = null;
Sean Condon021f0fa2018-12-06 23:31:11 -080063 @Input() showHosts: boolean = false;
64 @Input() deviceLabelToggle: LabelToggle = LabelToggle.NONE;
65 @Input() hostLabelToggle: HostLabelToggle = HostLabelToggle.NONE;
Sean Condon0c577f62018-11-18 22:40:05 +000066 @Input() regionData: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
Sean Condon021f0fa2018-12-06 23:31:11 -080067 private graph: ForceDirectedGraph;
Sean Condon0c577f62018-11-18 22:40:05 +000068 private _options: { width, height } = { width: 800, height: 600 };
Sean Condonf4f54a12018-10-10 23:25:46 +010069
Sean Condon021f0fa2018-12-06 23:31:11 -080070 // References to the children of this component - these are created in the
71 // template view with the *ngFor and we get them by a query here
Sean Condon0c577f62018-11-18 22:40:05 +000072 @ViewChildren(DeviceNodeSvgComponent) devices: QueryList<DeviceNodeSvgComponent>;
Sean Condon021f0fa2018-12-06 23:31:11 -080073 @ViewChildren(HostNodeSvgComponent) hosts: QueryList<HostNodeSvgComponent>;
Sean Condonf4f54a12018-10-10 23:25:46 +010074
Sean Condon0c577f62018-11-18 22:40:05 +000075 constructor(
76 protected log: LogService,
77 protected is: IconService,
78 private ref: ChangeDetectorRef
79 ) {
80 this.selectedLink = null;
81 this.log.debug('ForceSvgComponent constructed');
82 }
83
84 /**
Sean Condon021f0fa2018-12-06 23:31:11 -080085 * Utility for extracting a node name from an endpoint string
86 * In some cases - have to remove the port number from the end of a device
87 * name
88 * @param endPtStr The end point name
89 */
90 private static extractNodeName(endPtStr: string): string {
91 const slash: number = endPtStr.indexOf('/');
92 if (slash === -1) {
93 return endPtStr;
94 } else {
95 const afterSlash = endPtStr.substr(slash + 1);
96 if (afterSlash === 'None') {
97 return endPtStr;
98 } else {
99 return endPtStr.substr(0, slash);
100 }
101 }
102 }
103
104 @HostListener('window:resize', ['$event'])
105 onResize(event) {
106 this.graph.initSimulation(this.options);
107 this.log.debug('Simulation reinit after resize', event);
108 }
109
110 /**
Sean Condon0c577f62018-11-18 22:40:05 +0000111 * After the component is initialized create the Force simulation
Sean Condon021f0fa2018-12-06 23:31:11 -0800112 * The list of devices, hosts and links will not have been receieved back
113 * from the WebSocket yet as this time - they will be updated later through
114 * ngOnChanges()
Sean Condon0c577f62018-11-18 22:40:05 +0000115 */
116 ngOnInit() {
117 // Receiving an initialized simulated graph from our custom d3 service
118 this.graph = new ForceDirectedGraph(this.options);
119
120 /** Binding change detection check on each tick
Sean Condon021f0fa2018-12-06 23:31:11 -0800121 * This along with an onPush change detection strategy should enforce
122 * checking only when relevant! This improves scripting computation
123 * duration in a couple of tests I've made, consistently. Also, it makes
124 * sense to avoid unnecessary checks when we are dealing only with
125 * simulations data binding.
Sean Condon0c577f62018-11-18 22:40:05 +0000126 */
127 this.graph.ticker.subscribe((simulation) => {
128 // this.log.debug("Force simulation has ticked", simulation);
129 this.ref.markForCheck();
130 });
131 this.log.debug('ForceSvgComponent initialized - waiting for nodes and links');
132
133 this.is.loadIconDef('m_switch');
Sean Condon021f0fa2018-12-06 23:31:11 -0800134 this.is.loadIconDef('m_roadm');
135 this.is.loadIconDef('m_router');
136 this.is.loadIconDef('m_endstation');
Sean Condon0c577f62018-11-18 22:40:05 +0000137 }
138
139 /**
Sean Condon021f0fa2018-12-06 23:31:11 -0800140 * When any one of the inputs get changed by a containing component, this
141 * gets called automatically. In addition this is called manually by
142 * topology.service when a response is received from the WebSocket from the
143 * server
Sean Condon0c577f62018-11-18 22:40:05 +0000144 *
145 * The Devices, Hosts and SubRegions are all added to the Node list for the simulation
146 * The Links are added to the Link list of the simulation.
147 * Before they are added the Links are associated with Nodes based on their endPt
148 *
149 * @param changes - a list of changed @Input(s)
150 */
151 ngOnChanges(changes: SimpleChanges) {
152 if (changes['regionData']) {
153 const devices: Device[] =
154 changes['regionData'].currentValue.devices[this.visibleLayerIdx()];
155 const hosts: Host[] =
156 changes['regionData'].currentValue.hosts[this.visibleLayerIdx()];
157 const subRegions: SubRegion[] = changes['regionData'].currentValue.subRegion;
158 this.graph.nodes = [];
159 if (devices) {
160 this.graph.nodes = devices;
161 }
162 if (hosts) {
163 this.graph.nodes = this.graph.nodes.concat(hosts);
164 }
165 if (subRegions) {
166 this.graph.nodes = this.graph.nodes.concat(subRegions);
167 }
168
169 // Associate the endpoints of each link with a real node
170 this.graph.links = [];
171 for (const linkIdx of Object.keys(this.regionData.links)) {
Sean Condon021f0fa2018-12-06 23:31:11 -0800172 const epA = ForceSvgComponent.extractNodeName(
173 this.regionData.links[linkIdx].epA);
Sean Condon0c577f62018-11-18 22:40:05 +0000174 this.regionData.links[linkIdx].source =
175 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800176 node.id === epA);
177 const epB = ForceSvgComponent.extractNodeName(
178 this.regionData.links[linkIdx].epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000179 this.regionData.links[linkIdx].target =
180 this.graph.nodes.find((node) =>
Sean Condon021f0fa2018-12-06 23:31:11 -0800181 node.id === epB);
Sean Condon0c577f62018-11-18 22:40:05 +0000182 this.regionData.links[linkIdx].index = Number(linkIdx);
183 }
184
185 this.graph.links = this.regionData.links;
186
187 this.graph.initSimulation(this.options);
188 this.graph.initNodes();
189 this.log.debug('ForceSvgComponent input changed',
190 this.graph.nodes.length, 'nodes,', this.graph.links.length, 'links');
191 }
Sean Condon021f0fa2018-12-06 23:31:11 -0800192
193 if (changes['showHosts']) {
194 this.showHosts = changes['showHosts'].currentValue;
195 }
196
197 // Pass on the changes to device
198 if (changes['deviceLabelToggle']) {
199 this.deviceLabelToggle = changes['deviceLabelToggle'].currentValue;
200 this.devices.forEach((d) => {
201 d.ngOnChanges({'labelToggle': changes['deviceLabelToggle']});
202 });
203 }
204
205 // Pass on the changes to host
206 if (changes['hostLabelToggle']) {
207 this.hostLabelToggle = changes['hostLabelToggle'].currentValue;
208 this.hosts.forEach((h) => {
209 h.ngOnChanges({'labelToggle': changes['hostLabelToggle']});
210 });
211 }
212
213 this.ref.markForCheck();
Sean Condon0c577f62018-11-18 22:40:05 +0000214 }
215
216 /**
217 * Get the index of LayerType so it can drive the visibility of nodes and
218 * hosts on layers
219 */
220 visibleLayerIdx(): number {
221 const layerKeys: string[] = Object.keys(LayerType);
222 for (const idx in layerKeys) {
223 if (LayerType[layerKeys[idx]] === this.visibleLayer) {
224 return Number(idx);
225 }
226 }
227 return -1;
228 }
229
230 selectLink(link: RegionLink): void {
231 this.selectedLink = link;
232 this.linkSelected.emit(link);
233 }
234
235 get options() {
236 return this._options = {
237 width: window.innerWidth,
238 height: window.innerHeight
239 };
240 }
241
Sean Condon021f0fa2018-12-06 23:31:11 -0800242 /**
243 * Iterate through all hosts and devices to deselect the previously selected
244 * node. The emit an event to the parent that lets it know the selection has
245 * changed.
246 * @param selectedNode the newly selected node
247 */
248 updateSelected(selectedNode: Node): void {
249 this.log.debug('Device selected', selectedNode);
250 this.devices
251 .filter((d) =>
252 selectedNode === undefined || d.device.id !== selectedNode.id)
253 .forEach((d) => d.deselect());
254 this.hosts
255 .filter((h) =>
256 selectedNode === undefined || h.host.id !== selectedNode.id)
257 .forEach((h) => h.deselect());
258
259 this.selectedNodeEvent.emit(selectedNode);
Sean Condon0c577f62018-11-18 22:40:05 +0000260 }
261
Sean Condon021f0fa2018-12-06 23:31:11 -0800262 /**
263 * We want to filter links to show only those not related to hosts if the
264 * 'showHosts' flag has been switched off. If 'shwoHosts' is true, then
265 * display all links.
266 */
267 filteredLinks() {
268 return this.regionData.links.filter((h) =>
269 this.showHosts ||
270 ((<Host>h.source).nodeType !== 'host' &&
271 (<Host>h.target).nodeType !== 'host'));
Sean Condon0c577f62018-11-18 22:40:05 +0000272 }
Sean Condonf4f54a12018-10-10 23:25:46 +0100273}
Sean Condon0c577f62018-11-18 22:40:05 +0000274