Drawing slices, expanding and collapsing slices
Drawing forms
Added keybindings

Change-Id: Ic1f06eef20a6e1e7c0f1fea51752fe738f86d600
diff --git a/views/ngXosViews/mcord-slicing/src/js/aa b/views/ngXosViews/mcord-slicing/src/js/aa
new file mode 100644
index 0000000..0d4d9ae
--- /dev/null
+++ b/views/ngXosViews/mcord-slicing/src/js/aa
@@ -0,0 +1 @@
+that was the approach since it is completely independent from CORD or XOS, but if you prefere we can follow the same release scheme of CORD and from master pull the latest
\ No newline at end of file
diff --git a/views/ngXosViews/mcord-slicing/src/js/form.service.js b/views/ngXosViews/mcord-slicing/src/js/form.service.js
new file mode 100644
index 0000000..e18ab10
--- /dev/null
+++ b/views/ngXosViews/mcord-slicing/src/js/form.service.js
@@ -0,0 +1,178 @@
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 9/29/16.
+ */
+
+(function () {
+  'use strict';
+  angular.module('xos.mcord-slicing')
+  .service('FormHandler', function(LabelFormatter, XosFormHelpers){
+    const self = this;
+    const t = d3.transition()
+      .duration(500);
+
+    // draw a form nearby the node
+    this.drawForm = (node, linkGroup, formGroup) => {
+
+      // create an svg to draw a line from the node to the form
+      const line = linkGroup
+        .append('line')
+        .attr({
+          class: 'form-line',
+          id: `form-line-${node.type}-${node.id}`,
+          x1: node.x + 10,
+          y1: node.y,
+          x2: node.x + 10,
+          y2: node.y + 40,
+          opacity: 0,
+        });
+
+      line.transition(t)
+        .attr({
+          opacity: 1
+        });
+
+      // form container
+      const form = formGroup
+        .append('div')
+        .attr({
+          class: 'element-form',
+          id: `form-${node.type}-${node.id}`,
+        })
+        .style({
+          opacity: 0
+        });
+
+      const formEl = form
+        .append('form');
+
+      // cicle trough props (to be defined from rest)
+      this.addFormfields(node, formEl);
+
+      const buttonRow = formEl
+        .append('div')
+        .attr({
+          class: 'row'
+        });
+
+      buttonRow
+        .append('div')
+        .attr({
+          class: 'col-xs-6'
+        })
+        .append('a')
+        .attr({
+          class: 'btn btn-danger',
+          'data-parent-node-type': node.type,
+          'data-parent-node-id': node.id
+        })
+        .text('Close')
+        .on('click', function(){
+          self.removeForm(
+            d3.select(this).attr('data-parent-node-type'),
+            d3.select(this).attr('data-parent-node-id'),
+            linkGroup,
+            formGroup
+          );
+        });
+
+      buttonRow
+        .append('div')
+        .attr({
+          class: 'col-xs-6'
+        })
+        .append('button')
+        .attr({
+          type: 'button',
+          class: 'btn btn-success'
+        })
+        .text('Save')
+        .on('click', function(){
+          $(`#form-${node.type}-${node.id} input`).each(function(){
+            let input = $(this); // This is the jquery object of the input, do what you will
+            let val = input.val();
+            let name = input.attr('name');
+            console.log(name, val);
+          });
+        });
+
+      form.transition(t)
+        .style({
+          opacity: 1,
+          top: `${node.y + 95}px`,
+          left: `${node.x}px`
+        });
+    };
+
+    this.removeForm = (nodeType, nodeId, linkGroup, formGroup) => {
+      this.removeFormByParentNode({type: nodeType, id: nodeId}, linkGroup, formGroup);
+    };
+
+    // remove a form starting form his parent node
+    this.removeFormByParentNode = (node, linkGroup, formGroup) => {
+      // remove form
+      formGroup.selectAll(`#form-${node.type}-${node.id}`)
+        .transition(t)
+        .style({
+          opacity: 0
+        })
+        .remove();
+      // remove link
+      linkGroup.selectAll(`#form-line-${node.type}-${node.id}`)
+        .transition(t)
+        .attr({
+          opacity: 0
+        })
+        .remove();
+    };
+
+    this.getFieldValue = (val, fieldType) => {
+      if(fieldType === 'date'){
+        val = new Date(val);
+        val = `${val.getFullYear()}-${('0' + val.getMonth() + 1).slice(-2)}-${('0' + val.getDate()).slice(-2)}`
+      }
+      return val || '';
+    };
+
+    this.addFormField = (fieldName, fieldValue, formEl) => {
+      let fieldType = XosFormHelpers._getFieldFormat(fieldValue);
+      formEl
+        .append('div')
+        .attr({
+          class: 'row'
+        })
+        .append('div')
+        .attr({
+          class: 'col-xs-12'
+        })
+        .append('label')
+        .text(fieldName ? LabelFormatter.format(fieldName ) : 'test')
+        .append('input')
+        .attr({
+          type: fieldType,
+          name: fieldName,
+          value: this.getFieldValue(fieldValue, fieldType),
+          class: 'form-control'
+        });
+    };
+
+    this.addFormfields = (node, formEl) => {
+
+      this.addFormField('name', node.name, formEl);
+      // tmp check
+      if(!node.model){
+        return this.addFormField(null, null, formEl);
+      }
+
+      // create a list of fields to be printed
+      const fields = Object.keys(node.model);
+      _.forEach(fields, f => {
+        this.addFormField(f, node.model[f], formEl);
+      });
+    };
+  });
+})();
+
diff --git a/views/ngXosViews/mcord-slicing/src/js/graph.service.js b/views/ngXosViews/mcord-slicing/src/js/graph.service.js
new file mode 100644
index 0000000..223be9f
--- /dev/null
+++ b/views/ngXosViews/mcord-slicing/src/js/graph.service.js
@@ -0,0 +1,291 @@
+(function () {
+  'use strict';
+
+  angular.module('xos.mcord-slicing')
+  .service('SliceGraph', function(_, NodePositioner){
+    const g = new graphlib.Graph();
+
+    /**
+    * @ngdoc method
+    * @name xos.mcord-slicing.SliceGraph#buildGraph
+    * @methodOf xos.mcord-slicing.SliceGraph
+    * @description
+    * buildGraph
+    * @param {object} data An object in the for of {nodes: [], links: []} describing the graph
+    * @returns {null}
+    **/
+    this.buildGraph = (data) => {
+      _.forEach(data.nodes, n => g.setNode(n.id, n));
+      _.forEach(data.links, n => g.setEdge(n.source, n.target, n));
+    };
+
+    this.getLinks = () => {
+      return g.edges().map(e => {
+        return {
+          source: g.node(e.v),
+          target: g.node(e.w),
+          data: g.edge(e)
+        }
+      });
+    }
+
+    this.getGraph = () => g;
+
+    // find the successor of a node
+    this.getNodeSuccessors = (node) => {
+      return _.map(g.successors(node.id), n => {
+        return g.node(n);
+      })
+    };
+
+    this.getNodePredecessors = (node) => {
+      return _.map(g.predecessors(node.id), n => {
+        return g.node(n);
+      });
+    };
+
+    // get data plane successors of a node
+    this.getNodeDataPlaneSuccessors = (node) => {
+      return _.filter(this.getNodeSuccessors(node), n => {
+        return n.plane === 'data';
+      });
+    };
+
+    // find the end of the graph toward upstream
+    this.getUpstreamSinks = (el) => {
+      const sinks =  _.reduce(g.sinks(), (sinks, node, i) => {
+        let n = g.node(node);
+        if(n.type === 'upstream'){
+          sinks.push(n);
+        }
+        return sinks;
+      }, []);
+
+      return _.map(sinks, (s, i) => {
+        s.position = {
+          top: 0,
+          bottom: el.clientHeight,
+          total: sinks.length,
+          index: i + 1
+        };
+        return s;
+      })
+    };
+
+    this.positionGraph = (el) => {
+      // get root node
+      let nodes = this.getUpstreamSinks(el);
+
+      // find children, recursively
+      let children = [];
+      _.forEach(nodes, (n, i) => {
+        children = children.concat(this.findPredecessor(n));
+      });
+      nodes = nodes.concat(children);
+
+      // calculate the position for all nodes
+      nodes = _.map(nodes, r => {
+        return NodePositioner.getDataPlaneNodePos(r, el);
+      });
+
+      return nodes;
+    };
+
+    // this iterate on all the nodes, and add position information
+    this.findPredecessor = (node) => {
+      let preds = g.predecessors(node.id);
+
+      // saving predecessor information
+      preds = preds.map((p, i) => {
+        p = g.node(p);
+        const parentAvailableSpace = (node.position.bottom - node.position.top) / node.position.total;
+        const parentY = NodePositioner.getVpos(node);
+        p.position = {
+          top: parentY - (parentAvailableSpace / 2),
+          bottom: (parentY + (parentAvailableSpace / 2)),
+          total: preds.length,
+          index: i + 1
+        };
+        return p;
+      });
+
+      //recurse
+      const predsChild = _.reduce(preds, (list, p) => {
+        return list.concat(this.findPredecessor(p));
+      }, []);
+
+      return preds.concat(predsChild);
+    };
+
+    this.getGraphLinks = (nodes) => {
+      const links = [];
+      _.forEach(nodes, n => {
+        const edges = g.inEdges(n.id);
+        _.forEach(edges, e => {
+          links.push({
+            source: g.node(e.v),
+            target: g.node(e.w),
+            data: g.edge(e)
+          });
+        });
+      });
+      return links;
+    };
+
+    this.getDataPlaneForSlice = (ranRu, sliceId) => {
+      // hardcoded, likely to be improved
+      const ranCu = g.node(g.successors(ranRu.id)[0]);
+      const sgw = g.node(g.successors(ranCu.id)[0]);
+      const pgw = g.node(g.successors(sgw.id)[0]);
+
+      // augmenting nodes with sliceId
+      ranRu.sliceId = sliceId;
+      ranCu.sliceId = sliceId;
+      sgw.sliceId = sliceId;
+      pgw.sliceId = sliceId;
+      return [ranRu, ranCu, sgw, pgw];
+    };
+
+    this.getControlPlaneForSlice = (dataPlane, sliceId) => {
+      return _.reduce(dataPlane, (cp_nodes, dp_node) => {
+        // NOTE: looks that all the time the cplane version of the node is successors number 1, we may need to check
+        let cp_node = g.node(g.successors(dp_node.id)[1]);
+
+        // position relative to their data-plane node
+        cp_node = NodePositioner.getControlPlaneNodePos(cp_node, dp_node);
+        cp_node.sliceId = sliceId;
+        // hardcoded
+        // if control plane node is a sgw, there is an MME attached
+        if(cp_node.type === 'sgw'){
+          let mme = g.node(g.successors(cp_node.id)[1]);
+          // position relative to their data-plane node
+          mme = NodePositioner.getControlPlaneNodePos(mme, cp_node);
+          mme.sliceId = sliceId;
+          cp_nodes.push(mme);
+        }
+
+        return cp_nodes.concat(cp_node);
+      }, []);
+    };
+
+    this.activeSlices = [];
+    // this.usedSlicesId = [];
+    this.getSliceDetail= (node) => {
+      if(node.sliceId && this.activeSlices.indexOf(node.sliceId) > -1){
+        // the slice is already active, return an empty set
+        return [[], []];
+      }
+
+      // let sliceId;
+      // if (node.sliceId){
+      //   sliceId = node.sliceId;
+      // }
+      // else{
+        const sliceId = _.min(this.activeSlices) ? _.min(this.activeSlices) + 1 : 1;
+      // }
+      this.activeSlices.push(sliceId);
+      // this.usedSlicesId.push(sliceId);
+
+      // getting the beginning of the slice
+      const ranRu = (function getRanRu(n) {
+        if(n.type === 'ran-ru'){
+          return n;
+        }
+        // we assume that in the slice node have only one predecessor
+        const pred = g.predecessors(n.id);
+        return getRanRu(g.node(pred[0]));
+      })(node);
+
+      // get data plane nodes for this slice (need them to get the corresponding control plane)
+      const dp_nodes = this.getDataPlaneForSlice(ranRu, sliceId);
+      // get control plane nodes for this slice
+      const cp_nodes = this.getControlPlaneForSlice(dp_nodes, sliceId);
+
+      const links = this.getGraphLinks(cp_nodes);
+
+      // add a close button
+      let closeButton = {
+        name: 'Close',
+        id: `close-button-${sliceId}`,
+        type: 'button',
+        sliceId: sliceId
+      };
+      closeButton = NodePositioner.getControlPlaneNodePos(closeButton, cp_nodes[3]);
+      cp_nodes.push(closeButton);
+
+      return [cp_nodes, links];
+    };
+
+    this.removeActiveSlice = sliceId => {
+      // nodes are remove from the d3 nodes identified by id
+      this.activeSlices.splice(this.activeSlices.indexOf(sliceId), 1);
+    };
+
+  })
+  .service('NodePositioner', function(_, sliceElOrder){
+
+    let el;
+
+    this.storeEl = (_el) => {
+      el = _el;
+    };
+
+    this.getHpos = (node, el) => {
+      let elPos = sliceElOrder.indexOf(node.type) + 1;
+
+      // hardcoded
+      if(node.type === 'mme'){
+        elPos = sliceElOrder.indexOf('sgw') + 1
+      }
+      if(node.type === 'button'){
+        elPos = sliceElOrder.indexOf('pgw') + 1
+      }
+      let x = (el.clientWidth / (sliceElOrder.length + 1)) * elPos;
+      return x;
+    };
+
+    this.getVpos = (node) => {
+      // calculate the available space to distribute items
+      const availableSpace = node.position.bottom - node.position.top;
+
+      // calculate the distance between each item
+      const step = availableSpace / (node.position.total + 1);
+
+      // vertical position
+      const y = (step * node.position.index) + node.position.top;
+      return y;
+    };
+
+    // for nodes that are part of the data plane
+    this.getDataPlaneNodePos = (node) => {
+      const x = this.getHpos(node, el);
+      const y = this.getVpos(node);
+      node.x = x;
+      node.y = y;
+      node.transform = `translate(${x}, ${y})`;
+      node.fixed = true;
+      return node;
+    };
+
+    // control element nodes are positioned relatively to their corresponding data plane node
+    this.getControlPlaneNodePos = (cp_node, dp_node) => {
+      const x = this.getHpos(cp_node, el);
+      const y = dp_node.y - 75;
+      cp_node.x = x;
+      cp_node.y = y;
+      cp_node.transform = `translate(${x}, ${y})`;
+      cp_node.fixed = true;
+      return cp_node;
+    };
+
+  })
+  .value('sliceElOrder', [
+    'ue',
+    'profile',
+    'ran-ru',
+    'ran-cu',
+    'sgw',
+    'pgw',
+    'upstream'
+  ]);
+})();
diff --git a/views/ngXosViews/mcord-slicing/src/js/main.js b/views/ngXosViews/mcord-slicing/src/js/main.js
new file mode 100644
index 0000000..fbd9039
--- /dev/null
+++ b/views/ngXosViews/mcord-slicing/src/js/main.js
@@ -0,0 +1,93 @@
+'use strict';
+
+angular.module('xos.mcord-slicing', [
+  'ngResource',
+  'ngCookies',
+  'ui.router',
+  'xos.helpers'
+])
+.config(($stateProvider) => {
+  $stateProvider
+  .state('slicing-topo', {
+    url: '/',
+    template: '<slicing-topo></slicing-topo>'
+  })
+  .state('node-links', {
+    url: '/data',
+    template: '<node-links></node-links>'
+  });
+})
+.config(function($httpProvider){
+  $httpProvider.interceptors.push('NoHyperlinks');
+})
+.service('McordSlicingTopo', function($http, $q){
+  this.query = () => {
+    const d = $q.defer();
+
+    $http.get('api/service/mcord_slicing_ui/topology/')
+    .then((res) => {
+      let data;
+      if (res.data.hasOwnProperty('nodes')){
+        data = res.data;
+      }
+      else {
+        // INVESTIGATE why proxy change resposne
+        data = {
+          nodes: res.data[0],
+          links: res.data[1]
+        };
+      }
+      d.resolve(data);
+    })
+    .catch((e) => {
+      d.reject(e);
+    });
+
+    return {$promise: d.promise};
+  };
+})
+.directive('nodeLinks', function(){
+  return {
+    restrict: 'E',
+    scope: {},
+    bindToController: true,
+    controllerAs: 'vm',
+    templateUrl: 'templates/node-links.tpl.html',
+    controller: function(McordSlicingTopo){
+
+      this.tableConfig = {
+        columns: [
+          {
+            label: 'Id',
+            prop: 'id'
+          },
+          {
+            label: 'Name',
+            prop: 'name'
+          },
+          {
+            label: 'Type',
+            prop: 'type'
+          },
+          {
+            label: 'Plane',
+            prop: 'plane'
+          },
+          {
+            label: 'Model Id',
+            prop: 'model_id'
+          }
+        ]
+      };
+      
+      // retrieving user list
+      McordSlicingTopo.query().$promise
+      .then((users) => {
+        this.users = users.nodes;
+      })
+      .catch((e) => {
+        throw new Error(e);
+      });
+    }
+  };
+});
\ No newline at end of file
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