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