blob: f22876969e5dbf2d9389c4842c3629c899e17580 [file] [log] [blame]
var mercury = require('mercury');
var insertCss = require('insert-css');
var vis = require('vis');
var namespaceService = require('../../../../services/namespace/service');
// var getServiceIcon = require('../../get-service-icon');
var log = require('../../../../lib/log'
)('components:browse:items:visualize-view');
var css = require('./index.css');
var h = mercury.h;
module.exports = create;
module.exports.render = render;
// Matches HEX value of --color-text-primary in theme.css
var TEXT_COLOR = '#333333';
// Matches --color-bright in theme.css
var NODE_COLOR = '#6B0E9C';
// Matches --color-grey-dark in theme.css
var NODE_BORDER = '#263238';
// Matches --color-dark in theme.css
var ROOT_NODE_COLOR = '#4A068E';
function create() {}
function render(itemsState, browseState, browseEvents, navEvents) {
insertCss(css);
return [
new TreeWidget(browseState, browseEvents),
h('div.vismenu', { // visualization menu
}, [
// to add hierarchy view, add this button and the vismode handler
// h('paper-fab.mode', {
// attributes: {
// mini: true,
// icon: 'image:grain',
// title: 'change mode',
// 'aria-label': 'change mode'
// },
// 'ev-tap': vismode
// }),
h('paper-fab.zoom', {
attributes: {
mini: true,
icon: 'add',
title: 'zoom in',
'aria-label': 'zoom in'
},
'ev-down': zoom.bind(undefined, true),
'ev-up': stopzoom
}),
h('paper-fab.zoom', {
attributes: {
mini: true,
icon: 'remove',
title: 'zoom out',
'aria-label': 'zoom out'
},
'ev-down': zoom.bind(undefined, false),
'ev-up': stopzoom
}),
])
];
}
// The visjs visualization
var network, rootNodeId;
// change visualization between hierarchical and network modes
// function vismode() {
// network._restoreNodes();
// var el = document.querySelector('paper-fab.mode');
// if (network.constants.hierarchicalLayout.enabled) {
// el.className = 'mode';
// network.constants.hierarchicalLayout.enabled = false;
// network.constants.physics.hierarchicalRepulsion.enabled = false;
// network.constants.physics.barnesHut.enabled = true;
// network.focusOnNode(rootNodeId, { animation: true });
// network.zoomExtent(true);
// } else {
// el.className = 'mode selected';
// network.constants.physics.barnesHut.enabled = false;
// network.constants.physics.hierarchicalRepulsion = {
// enabled: true,
// nodeDistance: 70
// };
// network.constants.hierarchicalLayout.enabled = true;
// network.constants.physics.hierarchicalRepulsion.enabled = true;
// network.constants.physics.hierarchicalRepulsion.nodeDistance = 70;
// network._setupHierarchicalLayout();
// network.zoomExtent(true);
// }
// network._loadSelectedForceSolver();
// network.moving = true;
// network.start();
// }
var ZOOM_FACTOR = 0.01; // same as network.constants.keyboard.speed.zoom
// start zooming on button press
function zoom(zin, event) {
var zi = zin ? ZOOM_FACTOR : -ZOOM_FACTOR;
var selnodes; // selected nodes
if (event.shiftKey) {
selnodes = network.getSelection();
if (selnodes.nodes.length > 0) {
network.focusOnNode(selnodes.nodes[0], { animation: true });
} else {
network.zoomExtent(true);
}
}
network.zoomIncrement = zi;
network.start();
}
// stop zooming on button release
function stopzoom() {
network.zoomIncrement = 0;
}
// repaint visualization canvas when window resizes
window.addEventListener('resize', function redraw(e) {
if (network) {
network.redraw();
}
});
// Maximum number of levels that are automatically loaded below the root
var MAX_AUTO_LOAD_DEPTH = 4;
function TreeWidget(browseState, browseEvents) {
this.browseState = browseState;
this.browseEvents = browseEvents;
this.nodes = new vis.DataSet();
this.edges = new vis.DataSet();
}
TreeWidget.prototype.type = 'Widget';
// Dom element to initialize the network in
var networkElem;
TreeWidget.prototype.init = function() {
this.initNetworkElem();
requestAnimationFrame(this.updateNetwork.bind(this));
// wrap in a new element, needed for Mercury vdom to patch properly.
var wrapper = document.createElement('div');
wrapper.appendChild(networkElem);
return wrapper;
};
TreeWidget.prototype.initNetworkElem = function() {
if (!networkElem) {
networkElem = document.createElement('div');
networkElem.className = 'network';
}
};
// Keep track of previous namespace that was browsed to so we can
// know when navigating to a different namespace happens.
var previousNamespace;
TreeWidget.prototype.updateNetwork = function() {
if (network && network.constants.hierarchicalLayout.enabled) {
document.querySelector('paper-fab.mode').className = 'mode selected';
}
if (previousNamespace === this.browseState.namespace) {
return;
}
previousNamespace = this.browseState.namespace;
var self = this;
// Add the initial node.
rootNodeId = this.browseState.namespace;
this.nodes.add({
id: rootNodeId,
label: rootNodeId || '<root>',
level: 0,
shape: 'star',
color: {
background: ROOT_NODE_COLOR,
border: NODE_BORDER
}
});
// Load the subnodes.
this.rootNode = this.nodes.get(rootNodeId);
this.loadSubNodes(this.rootNode);
var options = {
hover: false,
selectable: true, // Need this or nodes won't be click-able
smoothCurves: false,
physics: {
barnesHut: {
enabled: true,
gravitationalConstant: -2200,
centralGravity: 0.2,
springLength: 64,
springConstant: 0.075,
damping: 0.12
}
},
hierarchicalLayout: {
enabled: false,
direction: 'LR',
nodeSpacing: 70,
levelSeparation: 180
},
keyboard: { speed: { x: -2, y: -2, zoom: 0.01 }},
edges: {
width: 1
},
nodes: {
radiusMin: 16,
radiusMax: 32,
fontColor: TEXT_COLOR
}
};
// Start drawing the network.
network = new vis.Network(networkElem, {
nodes: this.nodes,
edges: this.edges
}, options);
// Event listeners.
network.on('click', function onClick(data) {
// refresh side view
var nodeId = data.nodes[0];
var node = network.nodes[nodeId];
if (node) {
self.browseEvents.selectItem({
name: nodeId
});
}
});
network.on('doubleClick', function onClick(data) {
// drill
var nodeId = data.nodes[0];
var node = network.nodes[nodeId];
if (node && !node.subNodesLoaded) {
self.loadSubNodes(node);
}
});
return network;
};
TreeWidget.prototype.loadSubNodes = function(node) {
var namespace = node.id;
node.subNodesLoaded = true;
node.title = undefined;
var self = this;
namespaceService.getChildren(namespace).then(function(resultObservable) {
mercury.watch(resultObservable, function(results) {
// TODO(aghassemi) support removed and updated nodes when we switch to
// watchGlob
var existingIds = self.nodes.getIds();
var nodesToAdd = results.filter(function(item) {
var isNew = existingIds.indexOf(item.objectName) === -1;
return isNew;
});
var newNodes = nodesToAdd.map(function(item) {
var shape = 'dot';
var color = {
background: NODE_COLOR,
border: NODE_BORDER
};
return {
id: item.objectName,
label: item.mountedName,
level: node.level + 1,
shape: shape,
color: color
};
});
var newEdges = nodesToAdd.map(function(item) {
return {
from: namespace,
to: item.objectName,
color: TEXT_COLOR
};
});
newNodes.forEach(function(item) {
// recurse if within the MAX_AUTO_LOAD_DEPTH
if (item.level - self.rootNode.level < MAX_AUTO_LOAD_DEPTH) {
self.loadSubNodes(item);
} else {
item.title = 'Double-click to expand';
}
});
self.nodes.add(newNodes);
self.edges.add(newEdges);
});
}).catch(function(err) {
log.error('glob failed', err);
});
};
TreeWidget.prototype.update = function(prev, networkElem) {
requestAnimationFrame(this.updateNetwork.bind(this));
};