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