blob: 433f749f6c915ace1a07dda5d8e34637c0ee0818 [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 Component,
18 OnDestroy,
Sean Condon021f0fa2018-12-06 23:31:11 -080019 OnInit, SimpleChange,
Sean Condon0c577f62018-11-18 22:40:05 +000020 ViewChild
21} from '@angular/core';
Sean Condon55c30532018-10-29 12:26:57 +000022import * as d3 from 'd3';
Sean Condonf4f54a12018-10-10 23:25:46 +010023import {
24 FnService,
Sean Condon0c577f62018-11-18 22:40:05 +000025 KeysService,
26 KeysToken,
27 LogService,
28 PrefsService,
29 SvgUtilService,
30 WebSocketService,
31 Zoomer,
32 ZoomOpts,
33 ZoomService
Sean Condonf4f54a12018-10-10 23:25:46 +010034} from 'gui2-fw-lib';
Sean Condon0c577f62018-11-18 22:40:05 +000035import {InstanceComponent} from '../panel/instance/instance.component';
36import {SummaryComponent} from '../panel/summary/summary.component';
37import {DetailsComponent} from '../panel/details/details.component';
38import {BackgroundSvgComponent} from '../layer/backgroundsvg/backgroundsvg.component';
39import {ForceSvgComponent} from '../layer/forcesvg/forcesvg.component';
40import {TopologyService} from '../topology.service';
Sean Condon021f0fa2018-12-06 23:31:11 -080041import {HostLabelToggle, LabelToggle, Node} from '../layer/forcesvg/models';
Sean Condonf4f54a12018-10-10 23:25:46 +010042
43/**
44 * ONOS GUI Topology View
45 *
46 * This Topology View component is the top level component in a hierarchy that
47 * comprises the whole Topology View
48 *
49 * There are three main parts (panels, graphical and breadcrumbs)
50 * The panel hierarchy
51 * |-- Instances Panel (shows ONOS instances)
52 * |-- Summary Panel (summary of ONOS)
53 * |-- Toolbar Panel (the toolbar)
54 * |-- Details Panel (when a node is selected in the Force graphical view (see below))
55 *
56 * The graphical hierarchy contains
57 * Topology (this)
58 * |-- No Devices Connected (only of there are no nodes to show)
59 * |-- Zoom Layer (everything beneath this can be zoomed and panned)
60 * |-- Background (container for any backgrounds - can be toggled on and off)
61 * |-- Map
62 * |-- Forces (all of the nodes and links laid out by a d3.force simulation)
63 *
64 * The breadcrumbs
65 * |-- Breadcrumb (in region view a way of navigating back up through regions)
66 */
67@Component({
68 selector: 'onos-topology',
69 templateUrl: './topology.component.html',
70 styleUrls: ['./topology.component.css']
71})
Sean Condonaa4366d2018-11-02 14:29:01 +000072export class TopologyComponent implements OnInit, OnDestroy {
73 // These are references to the components inserted in the template
Sean Condonf4f54a12018-10-10 23:25:46 +010074 @ViewChild(InstanceComponent) instance: InstanceComponent;
75 @ViewChild(SummaryComponent) summary: SummaryComponent;
76 @ViewChild(DetailsComponent) details: DetailsComponent;
Sean Condonaa4366d2018-11-02 14:29:01 +000077 @ViewChild(BackgroundSvgComponent) background: BackgroundSvgComponent;
78 @ViewChild(ForceSvgComponent) force: ForceSvgComponent;
Sean Condonf4f54a12018-10-10 23:25:46 +010079
80 flashMsg: string = '';
81 prefsState = {};
Sean Condonf4f54a12018-10-10 23:25:46 +010082 hostLabelIdx: number = 1;
83
Sean Condon55c30532018-10-29 12:26:57 +000084 zoomer: Zoomer;
85 zoomEventListeners: any[];
86
Sean Condonf4f54a12018-10-10 23:25:46 +010087 constructor(
88 protected log: LogService,
89 protected fs: FnService,
90 protected ks: KeysService,
91 protected sus: SvgUtilService,
92 protected ps: PrefsService,
Sean Condon55c30532018-10-29 12:26:57 +000093 protected wss: WebSocketService,
Sean Condonaa4366d2018-11-02 14:29:01 +000094 protected zs: ZoomService,
95 protected ts: TopologyService,
Sean Condonf4f54a12018-10-10 23:25:46 +010096 ) {
97
98 this.log.debug('Topology component constructed');
99 }
100
Sean Condon021f0fa2018-12-06 23:31:11 -0800101 private static deviceLabelFlashMessage(index: number): string {
102 switch (index) {
103 case 0: return 'Hide device labels';
104 case 1: return 'Show friendly device labels';
105 case 2: return 'Show device ID labels';
106 }
107 }
108
109 private static hostLabelFlashMessage(index: number): string {
110 switch (index) {
111 case 0: return 'Hide host labels';
112 case 1: return 'Show friendly host labels';
113 case 2: return 'Show host IP labels';
114 case 3: return 'Show host MAC Address labels';
115 }
116 }
117
Sean Condonf4f54a12018-10-10 23:25:46 +0100118 ngOnInit() {
119 this.bindCommands();
Sean Condon55c30532018-10-29 12:26:57 +0000120 this.zoomer = this.createZoomer(<ZoomOpts>{
121 svg: d3.select('svg#topo2'),
122 zoomLayer: d3.select('g#topo-zoomlayer'),
123 zoomEnabled: () => true,
124 zoomMin: 0.25,
125 zoomMax: 10.0,
126 zoomCallback: (() => { return; })
127 });
128 this.zoomEventListeners = [];
Sean Condonaa4366d2018-11-02 14:29:01 +0000129 // The components from the template are handed over to TopologyService here
130 // so that WebSocket responses can be passed back in to them
131 // The handling of the WebSocket call is delegated out to the Topology
132 // Service just to compartmentalize things a bit
133 this.ts.init(this.instance, this.background, this.force);
Sean Condonf4f54a12018-10-10 23:25:46 +0100134 this.log.debug('Topology component initialized');
135 }
136
Sean Condonaa4366d2018-11-02 14:29:01 +0000137 ngOnDestroy() {
138 this.ts.destroy();
139 this.log.debug('Topology component destroyed');
140 }
141
Sean Condonf4f54a12018-10-10 23:25:46 +0100142 actionMap() {
143 return {
144 L: [() => {this.cycleDeviceLabels(); }, 'Cycle device labels'],
145 B: [(token) => {this.toggleBackground(token); }, 'Toggle background'],
146 D: [(token) => {this.toggleDetails(token); }, 'Toggle details panel'],
147 I: [(token) => {this.toggleInstancePanel(token); }, 'Toggle ONOS Instance Panel'],
148 O: [() => {this.toggleSummary(); }, 'Toggle the Summary Panel'],
149 R: [() => {this.resetZoom(); }, 'Reset pan / zoom'],
Sean Condon55c30532018-10-29 12:26:57 +0000150 'shift-Z': [() => {this.panAndZoom([0, 0], this.zoomer.scale() * 2); }, 'Zoom x2'],
151 'alt-Z': [() => {this.panAndZoom([0, 0], this.zoomer.scale() / 2); }, 'Zoom x0.5'],
Sean Condonf4f54a12018-10-10 23:25:46 +0100152 P: [(token) => {this.togglePorts(token); }, 'Toggle Port Highlighting'],
153 E: [() => {this.equalizeMasters(); }, 'Equalize mastership roles'],
154 X: [() => {this.resetNodeLocation(); }, 'Reset Node Location'],
155 U: [() => {this.unpinNode(); }, 'Unpin node (mouse over)'],
156 H: [() => {this.toggleHosts(); }, 'Toggle host visibility'],
157 M: [() => {this.toggleOfflineDevices(); }, 'Toggle offline visibility'],
158 dot: [() => {this.toggleToolbar(); }, 'Toggle Toolbar'],
159 'shift-L': [() => {this.cycleHostLabels(); }, 'Cycle host labels'],
160
161 // -- instance color palette debug
Sean Condon55c30532018-10-29 12:26:57 +0000162 9: () => {
163 this.sus.cat7().testCard(d3.select('svg#topo2'));
Sean Condonf4f54a12018-10-10 23:25:46 +0100164 },
165
166 esc: this.handleEscape,
167
168 // TODO update after adding in Background Service
169 // topology overlay selections
170 // F1: function () { t2tbs.fnKey(0); },
171 // F2: function () { t2tbs.fnKey(1); },
172 // F3: function () { t2tbs.fnKey(2); },
173 // F4: function () { t2tbs.fnKey(3); },
174 // F5: function () { t2tbs.fnKey(4); },
175 //
176 // _keyListener: t2tbs.keyListener.bind(t2tbs),
177
178 _helpFormat: [
179 ['I', 'O', 'D', 'H', 'M', 'P', 'dash', 'B'],
180 ['X', 'Z', 'N', 'L', 'shift-L', 'U', 'R', 'E', 'dot'],
181 [], // this column reserved for overlay actions
182 ],
183 };
184 }
185
186
187 bindCommands(additional?: any) {
188
189 const am = this.actionMap();
190 const add = this.fs.isO(additional);
191
192 // TODO: Reimplement when we have a use case
193 // if (add) {
194 // _.each(add, function (value, key) {
195 // // filter out meta properties (e.g. _keyOrder)
196 // if (!(_.startsWith(key, '_'))) {
197 // // don't allow re-definition of existing key bindings
198 // if (am[key]) {
199 // this.log.warn('keybind: ' + key + ' already exists');
200 // } else {
201 // am[key] = [value.cb, value.tt];
202 // }
203 // }
204 // });
205 // }
206
207 this.ks.keyBindings(am);
208
209 this.ks.gestureNotes([
210 ['click', 'Select the item and show details'],
211 ['shift-click', 'Toggle selection state'],
212 ['drag', 'Reposition (and pin) device / host'],
213 ['cmd-scroll', 'Zoom in / out'],
214 ['cmd-drag', 'Pan'],
215 ]);
216 }
217
218 handleEscape() {
219
220 if (false) {
221 // TODO: Cancel show mastership
222 // TODO: Cancel Active overlay
223 // TODO: Reinstate with components
224 } else {
225 this.log.debug('Handling escape');
226 // } else if (t2rs.deselectAllNodes()) {
227 // // else if we have node selections, deselect them all
228 // // (work already done)
229 // } else if (t2rs.deselectLink()) {
230 // // else if we have a link selection, deselect it
231 // // (work already done)
232 // } else if (t2is.isVisible()) {
233 // // If the instance panel is visible, close it
234 // t2is.toggle();
235 // } else if (t2sp.isVisible()) {
236 // // If the summary panel is visible, close it
237 // t2sp.toggle();
238 }
239 }
240
241
242
243 updatePrefsState(what, b) {
244 this.prefsState[what] = b ? 1 : 0;
245 this.ps.setPrefs('topo2_prefs', this.prefsState);
246 }
247
Sean Condonf4f54a12018-10-10 23:25:46 +0100248 protected cycleDeviceLabels() {
Sean Condon021f0fa2018-12-06 23:31:11 -0800249 const old: LabelToggle = this.force.deviceLabelToggle;
250 const next = LabelToggle.next(old);
251 this.force.ngOnChanges({'deviceLabelToggle':
252 new SimpleChange(old, next, false)});
253 this.flashMsg = TopologyComponent.deviceLabelFlashMessage(next);
254 this.log.debug('Cycling device labels', old, next);
Sean Condonf4f54a12018-10-10 23:25:46 +0100255 }
256
257 protected cycleHostLabels() {
Sean Condon021f0fa2018-12-06 23:31:11 -0800258 const old: HostLabelToggle = this.force.hostLabelToggle;
259 const next = HostLabelToggle.next(old);
260 this.force.ngOnChanges({'hostLabelToggle':
261 new SimpleChange(old, next, false)});
262 this.flashMsg = TopologyComponent.hostLabelFlashMessage(next);
263 this.log.debug('Cycling host labels', old, next);
Sean Condonf4f54a12018-10-10 23:25:46 +0100264 }
265
266 protected toggleBackground(token: KeysToken) {
267 this.flashMsg = 'Toggling background';
268 this.log.debug('Toggling background', token);
269 // TODO: Reinstate with components
270 // t2bgs.toggle(x);
271 }
272
273 protected toggleDetails(token: KeysToken) {
Sean Condon0c577f62018-11-18 22:40:05 +0000274 if (this.details.selectedNode) {
275 this.flashMsg = 'Toggling details';
276 this.details.togglePanel(() => {
277 });
278 this.log.debug('Toggling details', token);
279 }
Sean Condonf4f54a12018-10-10 23:25:46 +0100280 }
281
282 protected toggleInstancePanel(token: KeysToken) {
283 this.flashMsg = 'Toggling instances';
284 this.instance.togglePanel(() => {});
285 this.log.debug('Toggling instances', token);
286 // TODO: Reinstate with components
287 // this.updatePrefsState('insts', t2is.toggle(x));
288 }
289
290 protected toggleSummary() {
291 this.flashMsg = 'Toggling summary';
292 this.summary.togglePanel(() => {});
293 }
294
295 protected resetZoom() {
Sean Condon55c30532018-10-29 12:26:57 +0000296 this.zoomer.reset();
Sean Condonf4f54a12018-10-10 23:25:46 +0100297 this.log.debug('resetting zoom');
298 // TODO: Reinstate with components
299 // t2bgs.resetZoom();
300 // flash.flash('Pan and zoom reset');
301 }
302
303 protected togglePorts(token: KeysToken) {
304 this.log.debug('Toggling ports');
305 // TODO: Reinstate with components
306 // this.updatePrefsState('porthl', t2vs.togglePortHighlights(x));
307 // t2fs.updateLinks();
308 }
309
310 protected equalizeMasters() {
311 this.wss.sendEvent('equalizeMasters', null);
312
313 this.log.debug('equalizing masters');
314 // TODO: Reinstate with components
315 // flash.flash('Equalizing master roles');
316 }
317
318 protected resetNodeLocation() {
319 this.log.debug('resetting node location');
320 // TODO: Reinstate with components
321 // t2fs.resetNodeLocation();
322 // flash.flash('Reset node locations');
323 }
324
325 protected unpinNode() {
326 this.log.debug('unpinning node');
327 // TODO: Reinstate with components
328 // t2fs.unpin();
329 // flash.flash('Unpin node');
330 }
331
332 protected toggleToolbar() {
333 this.log.debug('toggling toolbar');
334 // TODO: Reinstate with components
335 // t2tbs.toggle();
336 }
337
338 protected actionedFlashed(action, message) {
339 this.log.debug('action flashed');
340 // TODO: Reinstate with components
341 // this.flash.flash(action + ' ' + message);
342 }
343
344 protected toggleHosts() {
Sean Condon021f0fa2018-12-06 23:31:11 -0800345 const old: boolean = this.force.showHosts;
346 const current = !this.force.showHosts;
347 this.force.ngOnChanges({'showHosts': new SimpleChange(old, current, false)});
348 this.flashMsg = (this.force.showHosts ? 'Show' : 'Hide') + ' Hosts';
349 this.log.debug('toggling hosts: ', this.force.showHosts ? 'Show' : 'Hide');
Sean Condonf4f54a12018-10-10 23:25:46 +0100350 }
351
352 protected toggleOfflineDevices() {
353 this.log.debug('toggling offline devices');
354 // TODO: Reinstate with components
355 // let on = t2rs.toggleOfflineDevices();
356 // this.actionedFlashed(on ? 'Show': 'Hide', 'offline devices');
357 }
358
359 protected notValid(what) {
360 this.log.warn('topo.js getActionEntry(): Not a valid ' + what);
361 }
362
363 getActionEntry(key) {
364 let entry;
365
366 if (!key) {
367 this.notValid('key');
368 return null;
369 }
370
371 entry = this.actionMap()[key];
372
373 if (!entry) {
374 this.notValid('actionMap (' + key + ') entry');
375 return null;
376 }
377 return this.fs.isA(entry) || [entry, ''];
378 }
379
Sean Condon55c30532018-10-29 12:26:57 +0000380
381
382 protected createZoomer(options: ZoomOpts) {
383 // need to wrap the original zoom callback to extend its behavior
384 const origCallback = this.fs.isF(options.zoomCallback) ? options.zoomCallback : () => {};
385
386 options.zoomCallback = () => {
387 origCallback([0, 0], 1);
388
389 this.zoomEventListeners.forEach((ev) => ev(this.zoomer));
390 };
391
392 return this.zs.createZoomer(options);
393 }
394
395 getZoomer() {
396 return this.zoomer;
397 }
398
399 findZoomEventListener(ev) {
400 for (let i = 0, len = this.zoomEventListeners.length; i < len; i++) {
401 if (this.zoomEventListeners[i] === ev) {
402 return i;
403 }
404 }
405 return -1;
406 }
407
408 addZoomEventListener(callback) {
409 this.zoomEventListeners.push(callback);
410 }
411
412 removeZoomEventListener(callback) {
413 const evIndex = this.findZoomEventListener(callback);
414
415 if (evIndex !== -1) {
416 this.zoomEventListeners.splice(evIndex);
417 }
418 }
419
420 adjustmentScale(min: number, max: number): number {
421 let _scale = 1;
422 const size = (min + max) / 2;
423
424 if (size * this.scale() < max) {
425 _scale = min / (size * this.scale());
426 } else if (size * this.scale() > max) {
427 _scale = min / (size * this.scale());
428 }
429
430 return _scale;
431 }
432
433 scale(): number {
434 return this.zoomer.scale();
435 }
436
437 panAndZoom(translate: number[], scale: number, transition?: number) {
438 this.zoomer.panZoom(translate, scale, transition);
439 }
440
Sean Condon0c577f62018-11-18 22:40:05 +0000441 nodeSelected(node: Node) {
442 this.details.selectedNode = node;
443 this.details.on = Boolean(node);
444 }
445
Sean Condonf4f54a12018-10-10 23:25:46 +0100446}