blob: bf466379d640dd9ba47484f497f8781503dc75a6 [file] [log] [blame]
/*
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, OnInit, ViewChild } from '@angular/core';
import * as d3 from 'd3';
import {
FnService,
KeysService, KeysToken,
LogService, PrefsService,
SvgUtilService, WebSocketService, Zoomer, ZoomOpts, ZoomService
} from 'gui2-fw-lib';
import {InstanceComponent} from '../panel/instance/instance.component';
import {SummaryComponent} from '../panel/summary/summary.component';
import {DetailsComponent} from '../panel/details/details.component';
/**
* ONOS GUI Topology View
*
* This Topology View component is the top level component in a hierarchy that
* comprises the whole Topology View
*
* There are three main parts (panels, graphical and breadcrumbs)
* The panel hierarchy
* |-- Instances Panel (shows ONOS instances)
* |-- Summary Panel (summary of ONOS)
* |-- Toolbar Panel (the toolbar)
* |-- Details Panel (when a node is selected in the Force graphical view (see below))
*
* The graphical hierarchy contains
* Topology (this)
* |-- No Devices Connected (only of there are no nodes to show)
* |-- Zoom Layer (everything beneath this can be zoomed and panned)
* |-- Background (container for any backgrounds - can be toggled on and off)
* |-- Map
* |-- Forces (all of the nodes and links laid out by a d3.force simulation)
*
* The breadcrumbs
* |-- Breadcrumb (in region view a way of navigating back up through regions)
*/
@Component({
selector: 'onos-topology',
templateUrl: './topology.component.html',
styleUrls: ['./topology.component.css']
})
export class TopologyComponent implements OnInit {
@ViewChild(InstanceComponent) instance: InstanceComponent;
@ViewChild(SummaryComponent) summary: SummaryComponent;
@ViewChild(DetailsComponent) details: DetailsComponent;
flashMsg: string = '';
prefsState = {};
hostLabelIdx: number = 1;
zoomer: Zoomer;
zoomEventListeners: any[];
constructor(
protected log: LogService,
protected fs: FnService,
protected ks: KeysService,
protected sus: SvgUtilService,
protected ps: PrefsService,
protected wss: WebSocketService,
protected zs: ZoomService
) {
this.log.debug('Topology component constructed');
}
ngOnInit() {
this.bindCommands();
this.zoomer = this.createZoomer(<ZoomOpts>{
svg: d3.select('svg#topo2'),
zoomLayer: d3.select('g#topo-zoomlayer'),
zoomEnabled: () => true,
zoomMin: 0.25,
zoomMax: 10.0,
zoomCallback: (() => { return; })
});
this.zoomEventListeners = [];
this.log.debug('Topology component initialized');
}
actionMap() {
return {
L: [() => {this.cycleDeviceLabels(); }, 'Cycle device labels'],
B: [(token) => {this.toggleBackground(token); }, 'Toggle background'],
D: [(token) => {this.toggleDetails(token); }, 'Toggle details panel'],
I: [(token) => {this.toggleInstancePanel(token); }, 'Toggle ONOS Instance Panel'],
O: [() => {this.toggleSummary(); }, 'Toggle the Summary Panel'],
R: [() => {this.resetZoom(); }, 'Reset pan / zoom'],
'shift-Z': [() => {this.panAndZoom([0, 0], this.zoomer.scale() * 2); }, 'Zoom x2'],
'alt-Z': [() => {this.panAndZoom([0, 0], this.zoomer.scale() / 2); }, 'Zoom x0.5'],
P: [(token) => {this.togglePorts(token); }, 'Toggle Port Highlighting'],
E: [() => {this.equalizeMasters(); }, 'Equalize mastership roles'],
X: [() => {this.resetNodeLocation(); }, 'Reset Node Location'],
U: [() => {this.unpinNode(); }, 'Unpin node (mouse over)'],
H: [() => {this.toggleHosts(); }, 'Toggle host visibility'],
M: [() => {this.toggleOfflineDevices(); }, 'Toggle offline visibility'],
dot: [() => {this.toggleToolbar(); }, 'Toggle Toolbar'],
'shift-L': [() => {this.cycleHostLabels(); }, 'Cycle host labels'],
// -- instance color palette debug
9: () => {
this.sus.cat7().testCard(d3.select('svg#topo2'));
},
esc: this.handleEscape,
// TODO update after adding in Background Service
// topology overlay selections
// F1: function () { t2tbs.fnKey(0); },
// F2: function () { t2tbs.fnKey(1); },
// F3: function () { t2tbs.fnKey(2); },
// F4: function () { t2tbs.fnKey(3); },
// F5: function () { t2tbs.fnKey(4); },
//
// _keyListener: t2tbs.keyListener.bind(t2tbs),
_helpFormat: [
['I', 'O', 'D', 'H', 'M', 'P', 'dash', 'B'],
['X', 'Z', 'N', 'L', 'shift-L', 'U', 'R', 'E', 'dot'],
[], // this column reserved for overlay actions
],
};
}
bindCommands(additional?: any) {
const am = this.actionMap();
const add = this.fs.isO(additional);
// TODO: Reimplement when we have a use case
// if (add) {
// _.each(add, function (value, key) {
// // filter out meta properties (e.g. _keyOrder)
// if (!(_.startsWith(key, '_'))) {
// // don't allow re-definition of existing key bindings
// if (am[key]) {
// this.log.warn('keybind: ' + key + ' already exists');
// } else {
// am[key] = [value.cb, value.tt];
// }
// }
// });
// }
this.ks.keyBindings(am);
this.ks.gestureNotes([
['click', 'Select the item and show details'],
['shift-click', 'Toggle selection state'],
['drag', 'Reposition (and pin) device / host'],
['cmd-scroll', 'Zoom in / out'],
['cmd-drag', 'Pan'],
]);
}
handleEscape() {
if (false) {
// TODO: Cancel show mastership
// TODO: Cancel Active overlay
// TODO: Reinstate with components
} else {
this.log.debug('Handling escape');
// } else if (t2rs.deselectAllNodes()) {
// // else if we have node selections, deselect them all
// // (work already done)
// } else if (t2rs.deselectLink()) {
// // else if we have a link selection, deselect it
// // (work already done)
// } else if (t2is.isVisible()) {
// // If the instance panel is visible, close it
// t2is.toggle();
// } else if (t2sp.isVisible()) {
// // If the summary panel is visible, close it
// t2sp.toggle();
}
}
updatePrefsState(what, b) {
this.prefsState[what] = b ? 1 : 0;
this.ps.setPrefs('topo2_prefs', this.prefsState);
}
deviceLabelFlashMessage(index) {
switch (index) {
case 0: return 'Hide device labels';
case 1: return 'Show friendly device labels';
case 2: return 'Show device ID labels';
}
}
hostLabelFlashMessage(index) {
switch (index) {
case 0: return 'Hide host labels';
case 1: return 'Show friendly host labels';
case 2: return 'Show host IP labels';
case 3: return 'Show host MAC Address labels';
}
}
protected cycleDeviceLabels() {
this.log.debug('Cycling device labels');
// TODO: Reinstate with components
// let deviceLabelIndex = t2ps.get('dlbls') + 1;
// let newDeviceLabelIndex = deviceLabelIndex % 3;
//
// t2ps.set('dlbls', newDeviceLabelIndex);
// t2fs.updateNodes();
// flash.flash(deviceLabelFlashMessage(newDeviceLabelIndex));
}
protected cycleHostLabels() {
const hostLabelIndex = this.hostLabelIdx + 1;
this.hostLabelIdx = hostLabelIndex % 4;
this.flashMsg = this.hostLabelFlashMessage(this.hostLabelIdx);
this.log.debug('Cycling host labels');
// TODO: Reinstate with components
// t2ps.set('hlbls', newHostLabelIndex);
// t2fs.updateNodes();
}
protected toggleBackground(token: KeysToken) {
this.flashMsg = 'Toggling background';
this.log.debug('Toggling background', token);
// TODO: Reinstate with components
// t2bgs.toggle(x);
}
protected toggleDetails(token: KeysToken) {
this.flashMsg = 'Toggling details';
this.details.togglePanel(() => {});
this.log.debug('Toggling details', token);
}
protected toggleInstancePanel(token: KeysToken) {
this.flashMsg = 'Toggling instances';
this.instance.togglePanel(() => {});
this.log.debug('Toggling instances', token);
// TODO: Reinstate with components
// this.updatePrefsState('insts', t2is.toggle(x));
}
protected toggleSummary() {
this.flashMsg = 'Toggling summary';
this.summary.togglePanel(() => {});
}
protected resetZoom() {
this.zoomer.reset();
this.log.debug('resetting zoom');
// TODO: Reinstate with components
// t2bgs.resetZoom();
// flash.flash('Pan and zoom reset');
}
protected togglePorts(token: KeysToken) {
this.log.debug('Toggling ports');
// TODO: Reinstate with components
// this.updatePrefsState('porthl', t2vs.togglePortHighlights(x));
// t2fs.updateLinks();
}
protected equalizeMasters() {
this.wss.sendEvent('equalizeMasters', null);
this.log.debug('equalizing masters');
// TODO: Reinstate with components
// flash.flash('Equalizing master roles');
}
protected resetNodeLocation() {
this.log.debug('resetting node location');
// TODO: Reinstate with components
// t2fs.resetNodeLocation();
// flash.flash('Reset node locations');
}
protected unpinNode() {
this.log.debug('unpinning node');
// TODO: Reinstate with components
// t2fs.unpin();
// flash.flash('Unpin node');
}
protected toggleToolbar() {
this.log.debug('toggling toolbar');
// TODO: Reinstate with components
// t2tbs.toggle();
}
protected actionedFlashed(action, message) {
this.log.debug('action flashed');
// TODO: Reinstate with components
// this.flash.flash(action + ' ' + message);
}
protected toggleHosts() {
// this.flashMsg = on ? 'Show': 'Hide', 'Hosts';
this.log.debug('toggling hosts');
// TODO: Reinstate with components
// let on = t2rs.toggleHosts();
// this.actionedFlashed(on ? 'Show': 'Hide', 'Hosts');
}
protected toggleOfflineDevices() {
this.log.debug('toggling offline devices');
// TODO: Reinstate with components
// let on = t2rs.toggleOfflineDevices();
// this.actionedFlashed(on ? 'Show': 'Hide', 'offline devices');
}
protected notValid(what) {
this.log.warn('topo.js getActionEntry(): Not a valid ' + what);
}
getActionEntry(key) {
let entry;
if (!key) {
this.notValid('key');
return null;
}
entry = this.actionMap()[key];
if (!entry) {
this.notValid('actionMap (' + key + ') entry');
return null;
}
return this.fs.isA(entry) || [entry, ''];
}
protected createZoomer(options: ZoomOpts) {
// need to wrap the original zoom callback to extend its behavior
const origCallback = this.fs.isF(options.zoomCallback) ? options.zoomCallback : () => {};
options.zoomCallback = () => {
origCallback([0, 0], 1);
this.zoomEventListeners.forEach((ev) => ev(this.zoomer));
};
return this.zs.createZoomer(options);
}
getZoomer() {
return this.zoomer;
}
findZoomEventListener(ev) {
for (let i = 0, len = this.zoomEventListeners.length; i < len; i++) {
if (this.zoomEventListeners[i] === ev) {
return i;
}
}
return -1;
}
addZoomEventListener(callback) {
this.zoomEventListeners.push(callback);
}
removeZoomEventListener(callback) {
const evIndex = this.findZoomEventListener(callback);
if (evIndex !== -1) {
this.zoomEventListeners.splice(evIndex);
}
}
adjustmentScale(min: number, max: number): number {
let _scale = 1;
const size = (min + max) / 2;
if (size * this.scale() < max) {
_scale = min / (size * this.scale());
} else if (size * this.scale() > max) {
_scale = min / (size * this.scale());
}
return _scale;
}
scale(): number {
return this.zoomer.scale();
}
panAndZoom(translate: number[], scale: number, transition?: number) {
this.zoomer.panZoom(translate, scale, transition);
}
}