Drawing slices, expanding and collapsing slices
Drawing forms
Added keybindings
Change-Id: Ic1f06eef20a6e1e7c0f1fea51752fe738f86d600
diff --git a/views/ngXosViews/mcord-slicing/src/js/slicing-topo.directive.js b/views/ngXosViews/mcord-slicing/src/js/slicing-topo.directive.js
new file mode 100644
index 0000000..b91c8f4
--- /dev/null
+++ b/views/ngXosViews/mcord-slicing/src/js/slicing-topo.directive.js
@@ -0,0 +1,502 @@
+(function () {
+ 'use strict';
+
+ angular.module('xos.mcord-slicing')
+ .directive('slicingTopo', function(){
+ return {
+ restrict: 'E',
+ scope: {},
+ bindToController: true,
+ controllerAs: 'vm',
+ templateUrl: 'templates/slicing-topo.tpl.html',
+ controller: function($element, SliceGraph, McordSlicingTopo, _, NodePositioner, FormHandler){
+
+ let svg;
+ let nodes, links;
+ let nodeGroup, linkGroup, formGroup;
+ let dragLine, dragStartNode, dragEndNode, selectedLink;
+ let selectedNode, nodeSiblings;
+
+ var t = d3.transition()
+ .duration(500);
+
+ this.activeSlices = [];
+
+ const resetDragInfo = () => {
+ // reset drag nodes
+ dragStartNode = null;
+ dragEndNode = null;
+
+ // hide dragLine
+ dragLine
+ .classed('hidden', true);
+ };
+
+ McordSlicingTopo.query().$promise
+ .then((topology) => {
+ NodePositioner.storeEl($element[0]);
+ handleSvg($element[0]);
+ SliceGraph.buildGraph(topology);
+ _nodes = SliceGraph.positionGraph($element[0]);
+ _links = SliceGraph.getGraphLinks(_nodes);
+ drawGraph();
+ })
+ .catch((e) => {
+ throw new Error(e);
+ });
+
+ const handleSvg = (el) => {
+ this.el = el;
+ d3.select(el).select('svg').remove();
+
+ svg = d3.select(el)
+ .append('svg')
+ .style('width', `${el.clientWidth}px`)
+ .style('height', `${el.clientHeight}px`);
+
+ linkGroup = svg.append('g')
+ .attr({
+ class: 'link-group'
+ });
+
+ nodeGroup = svg.append('g')
+ .attr({
+ class: 'node-group'
+ });
+
+ formGroup = d3.select(el)
+ .append('div')
+ .attr({
+ class: 'form-container'
+ });
+
+ // line displayed when dragging nodes
+ dragLine = svg.append('svg:path')
+ .attr('class', 'dragline hidden')
+ .attr('d', 'M0,0L0,0');
+ };
+
+ const tick = () => {
+ svg.selectAll('.link')
+ .attr('x1', d => d.source.x)
+ .attr('y1', d => d.source.y)
+ .attr('x2', d => d.target.x)
+ .attr('y2', d => d.target.y);
+ };
+
+ // prepare the data to show all slices
+ let _nodes = [];
+ let _links = [];
+
+ // attach slice details
+ const attachSliceDetails = n => {
+ let [newNodes, newLinks] = SliceGraph.getSliceDetail(n);
+ _nodes = _nodes.concat(newNodes);
+ _links = _links.concat(newLinks);
+ drawGraph();
+ };
+
+ // remove slice detail
+ const removeSliceDetails = sliceId => {
+
+ SliceGraph.removeActiveSlice(sliceId);
+
+ // if the selected node is part of the slice I'm closing
+ // deselect the node
+ if(selectedNode && selectedNode.sliceId === sliceId){
+ selectedNode = null;
+ nodeSiblings = null;
+ }
+
+ // remove control plane nodes related to this slice
+ _nodes = _.filter(_nodes, n => {
+ if(n.sliceId === sliceId && (n.plane === 'control' || n.type === 'button')){
+ // if I remove the node check that there is no form attached
+ FormHandler.removeFormByParentNode(n, linkGroup, formGroup);
+ return false;
+ }
+ return true;
+ });
+
+ // remove sliceId from data plane element
+ _nodes = _.map(_nodes, n => {
+ if(n.sliceId === sliceId){
+ delete n.sliceId;
+ }
+ return n;
+ });
+
+ // remove control plane links related to this slice
+ _links = _.filter(_links, l => {
+ if(_.findIndex(_nodes, {id: l.data.source}) === -1){
+ return false;
+ }
+ if(_.findIndex(_nodes, {id: l.data.target}) === -1){
+ return false;
+ }
+ return true;
+ });
+ drawGraph();
+ };
+
+ const deleteLink = linkId => {
+ // TODO
+ // [ ] delete from graphlib
+ // [ ] delete from backend
+ console.log(_links);
+ _.remove(_links, (l) => {
+ return l.data.id === linkId;
+ });
+ console.log(_links);
+ drawGraph();
+ };
+
+ const expandNode = (n) => {
+ console.log('exp', n);
+ resetDragInfo();
+ const sliceComponents = ['ran-ru', 'ran-cu', 'pgw', 'sgw'];
+ if(sliceComponents.indexOf(n.type) > -1 && n.plane === 'data' && !n.sliceId){
+ attachSliceDetails(n);
+ }
+ else if (n.type === 'button'){
+ removeSliceDetails(n.sliceId);
+ }
+ else if (!n.formAttached && n.model){
+ n.formAttached = true;
+ FormHandler.drawForm(n, linkGroup, formGroup);
+ }
+ else if (n.formAttached){
+ n.formAttached = false;
+ FormHandler.removeFormByParentNode(n, linkGroup, formGroup);
+ }
+ };
+
+ const selectNextNode = () => {
+ if(!selectedNode){
+ selectedNode = _nodes[0];
+ selectedNode.selected = true;
+ }
+ else {
+ // TODO if no sliceId check only data plane successors
+ nodeSiblings = SliceGraph.getNodeSuccessors(selectedNode);
+
+ if(nodeSiblings.length === 0){
+ return;
+ };
+ // reset current selected node
+ selectedNode.selected = false;
+ // find next node
+ let nextNodeId = _.findIndex(_nodes, {id: nodeSiblings[0].id});
+ selectedNode = _nodes[nextNodeId];
+ selectedNode.selected = true;
+
+ // NOTE I need to update sibling with successor of the parent
+ // to enable vertical navigation
+ let parents = SliceGraph.getNodeSuccessors(selectedNode);
+ if(parents.lenght > 0){
+ nodeSiblings = SliceGraph.getNodePredecessors(parents[0]);
+ }
+ else {
+ nodeSiblings = null;
+ }
+ }
+ drawGraph();
+ };
+
+ const selectPrevNode = () => {
+ if(!selectedNode){
+ selectedNode = _nodes[0];
+ selectedNode.selected = true;
+ }
+ else {
+ nodeSiblings = SliceGraph.getNodePredecessors(selectedNode);
+
+ if(nodeSiblings.length === 0){
+ return;
+ };
+ // reset current selected node
+ selectedNode.selected = false;
+ // find next node
+ let prev = _.findIndex(_nodes, {id: nodeSiblings[0].id});
+
+ if(prev < 0){
+ prev = _nodes.length - 1;
+ }
+ selectedNode = _nodes[prev];
+ selectedNode.selected = true;
+ }
+ drawGraph();
+ };
+
+ const sortByY = (a, b) => {
+ if (a.y < b.y)
+ return 1;
+ if (a.y > b.y)
+ return -1;
+ return 0;
+ };
+
+ const getSameTypeNodes = (selectedNode) => {
+ return _.filter(_nodes, n => {
+ if(selectedNode.type === 'pgw' && n.type === 'button'){
+ return true;
+ }
+ if(selectedNode.type === 'button' && n.type === 'pgw'){
+ return true;
+ }
+ if (selectedNode.type === 'sgw' && n.type === 'mme'){
+ return true;
+ }
+ if (selectedNode.type === 'mme' && n.type === 'sgw'){
+ return true;
+ }
+ return n.type === selectedNode.type;
+ }).sort(sortByY);
+ };
+
+ const selectNextSibling = () => {
+ if(!selectedNode){
+ selectedNode = _nodes[0];
+ selectedNode.selected = true;
+ }
+ else {
+ // reset current selected node
+ selectedNode.selected = false;
+
+ // find next node
+ let sameTypeNodes = getSameTypeNodes(selectedNode);
+
+ let nextSiblingId = _.findIndex(sameTypeNodes, {id: selectedNode.id}) + 1;
+ if(nextSiblingId === sameTypeNodes.length){
+ nextSiblingId = 0;
+ }
+ let nextNodeId = _.findIndex(_nodes, {id: sameTypeNodes[nextSiblingId].id});
+ selectedNode = _nodes[nextNodeId];
+ selectedNode.selected = true;
+ }
+ drawGraph();
+ };
+
+ const selectPrevSibling = () => {
+ if(!selectedNode){
+ selectedNode = _nodes[0];
+ selectedNode.selected = true;
+ }
+ else {
+ // reset current selected node
+ selectedNode.selected = false;
+
+ // find next node
+ let sameTypeNodes = getSameTypeNodes(selectedNode);
+
+ let nextSiblingId = _.findIndex(sameTypeNodes, {id: selectedNode.id}) - 1;
+ if(nextSiblingId < 0){
+ nextSiblingId = sameTypeNodes.length - 1;
+ }
+ let nextNodeId = _.findIndex(_nodes, {id: sameTypeNodes[nextSiblingId].id});
+ selectedNode = _nodes[nextNodeId];
+ selectedNode.selected = true;
+ }
+ drawGraph();
+ };
+
+ const drawGraph = () => {
+
+ // svg.selectAll('.node-container').remove();
+ // svg.selectAll('.link-container').remove();
+
+ var force = d3.layout.force()
+ .nodes(_nodes)
+ .links(_links)
+ .charge(-1060)
+ .gravity(0.1)
+ .linkDistance(200)
+ .size([this.el.clientWidth, this.el.clientHeight])
+ .on('tick', tick)
+ .start();
+
+ links = linkGroup.selectAll('.link-container')
+ .data(_links, d => d.data.id)
+ .enter()
+ .insert('g')
+ .attr({
+ class: 'link-container',
+ opacity: 0
+ });
+
+ links
+ .transition(t)
+ .attr({
+ opacity: 1
+ });
+
+ links.insert('line')
+ .attr('class', d => `link ${d.data.plane}`)
+ .on('click', function(link ){
+ selectedLink = link;
+
+ // deselect all other links
+ d3.selectAll('.link').classed('selected', false);
+
+ d3.select(this).classed('selected', true);
+ });
+
+ nodes = nodeGroup.selectAll('.node')
+ .data(_nodes, d => d.id)
+ .attr({
+ class: d => `node ${d.plane} ${d.type} ${d.selected ? 'selected': ''}`,
+ });
+
+ nodes
+ .enter()
+ .append('g')
+ .attr({
+ class: 'node-container',
+ transform: d => d.transform,
+ opacity: 0
+ });
+
+ nodes.transition(t)
+ .attr({
+ opacity: 1
+ });
+
+ nodes.append('rect')
+ .attr({
+ class: d => `node ${d.plane} ${d.type} ${d.selected ? 'selected': ''}`,
+ width: 100,
+ height: 50,
+ x: -50,
+ y: -25
+ });
+
+ nodes.append('text')
+ .attr({
+ 'text-anchor': 'middle',
+ 'alignment-baseline': 'middle'
+ })
+ // .text(d => `${d.id} ${d.name}`);
+ .text(d => `${d.name}`);
+
+ nodes.on('click', (n) => {
+ expandNode(n);
+ });
+
+ nodes
+ .on('mousedown', (n) => {
+ // save a reference to dragStart
+ dragStartNode = n;
+
+ dragLine
+ .classed('hidden', false)
+ .attr('d', 'M' + dragStartNode.x + ',' + dragStartNode.y + 'L' + dragStartNode.x + ',' + dragStartNode.y);
+ })
+ .on('mouseover', (n) => {
+ if(dragStartNode){
+ dragEndNode = n;
+ }
+ });
+
+ svg
+ .on('mousemove', function(){
+ if(!dragStartNode){
+ return;
+ }
+ dragLine.attr('d', 'M' + dragStartNode.x + ',' + dragStartNode.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]);
+ })
+ .on('mouseup', () => {
+ if(!dragStartNode || !dragEndNode){
+ resetDragInfo();
+ return;
+ }
+
+ // TODO
+ // [X] check that I can connect the two nodes
+ // [X] check link direction
+ // [ ] save the new link in the BE
+
+ // check that I can connect the 2 nodes
+ const successorType = SliceGraph.getNodeDataPlaneSuccessors(dragStartNode)[0].type;
+ if(dragEndNode.type !== successorType){
+ resetDragInfo();
+ return;
+ }
+
+ // create the link
+ _links.push({
+ source: dragStartNode,
+ target: dragEndNode,
+ data: {
+ id: `${dragStartNode.id}.${dragEndNode.id}`,
+ source: dragStartNode.id,
+ target: dragEndNode.id
+ }
+ });
+
+ // update the graph
+ // TODO recalculate graph positions
+ drawGraph();
+
+ resetDragInfo();
+ });
+
+ // remove exiting nodes
+ svg.selectAll('.node-container')
+ .data(_nodes, d => d.id)
+ .exit()
+ .transition(t)
+ .attr({
+ opacity: 0
+ })
+ .remove();
+
+ // remove exiting links
+ svg.selectAll('.link-container')
+ .data(_links, d => d.data.id)
+ .exit()
+ .transition(t)
+ .attr({
+ opacity: 0
+ })
+ .remove();
+ };
+
+ d3.select('body')
+ .on('keydown', function(){
+ // console.log(d3.event.code);
+ if(d3.event.code === 'Backspace' && selectedLink){
+ // delete link
+ deleteLink(selectedLink.data.id);
+ }
+ if(d3.event.code === 'Enter' && selectedNode){
+ d3.event.preventDefault();
+ expandNode(selectedNode);
+ }
+ if(d3.event.code === 'Escape' && selectedNode){
+ selectedNode.selected = false;
+ selectedNode = null;
+ nodeSiblings = null;
+ drawGraph();
+ }
+ if(d3.event.code === 'ArrowRight'){
+ d3.event.preventDefault();
+ selectNextNode();
+ }
+ if(d3.event.code === 'ArrowLeft'){
+ d3.event.preventDefault();
+ selectPrevNode();
+ }
+ if(d3.event.code === 'ArrowUp'){
+ d3.event.preventDefault();
+ selectNextSibling();
+ }
+ if(d3.event.code === 'ArrowDown'){
+ d3.event.preventDefault();
+ selectPrevSibling();
+ }
+
+ });
+ }
+ }
+ });
+})();
\ No newline at end of file