← Back to team overview

yellow team mailing list archive

[Merge] lp:~makyo/juju-gui/topology-relations into lp:juju-gui

 

Matthew Scott has proposed merging lp:~makyo/juju-gui/topology-relations into lp:juju-gui.

Requested reviews:
  Juju GUI Hackers (juju-gui)
Related bugs:
  Bug #1077047 in juju-gui: "Create Relations topology module"
  https://bugs.launchpad.net/juju-gui/+bug/1077047

For more details, see:
https://code.launchpad.net/~makyo/juju-gui/topology-relations/+merge/141120

Relations Topology Module

Refactoring relations out of the mega topology module into its own module.  This is quite a big branch, and with undocumented methods and custom events, may get larger yet.  Requesting bcsaller as one reviewer.

https://codereview.appspot.com/6999047/

-- 
https://code.launchpad.net/~makyo/juju-gui/topology-relations/+merge/141120
Your team Juju GUI Hackers is requested to review the proposed merge of lp:~makyo/juju-gui/topology-relations into lp:juju-gui.
=== modified file 'app/assets/javascripts/d3-components.js'
--- app/assets/javascripts/d3-components.js	2012-12-20 17:03:24 +0000
+++ app/assets/javascripts/d3-components.js	2012-12-21 19:34:22 +0000
@@ -508,4 +508,5 @@
   'requires': ['d3',
     'base',
     'array-extras',
-    'event']});
+    'event',
+    'event-resize']});

=== modified file 'app/views/environment.js'
--- app/views/environment.js	2012-12-19 13:45:10 +0000
+++ app/views/environment.js	2012-12-21 19:34:22 +0000
@@ -51,6 +51,7 @@
             // Bind all the behaviors we need as modules.
             topo.addModule(views.MegaModule);
             topo.addModule(views.PanZoomModule);
+            topo.addModule(views.RelationModule);
 
             topo.addTarget(this);
             this.topo = topo;

=== modified file 'app/views/topology/mega.js'
--- app/views/topology/mega.js	2012-12-20 13:34:43 +0000
+++ app/views/topology/mega.js	2012-12-21 19:34:22 +0000
@@ -32,15 +32,9 @@
         '.service': {
           click: 'serviceClick',
           dblclick: 'serviceDblClick',
-          mouseenter: 'serviceMouseEnter',
           mouseleave: 'mousemove'
         },
 
-        '.sub-rel-block': {
-          mouseenter: 'subRelBlockMouseEnter',
-          mouseleave: 'subRelBlockMouseLeave',
-          click: 'subRelBlockClick'
-        },
         '.service-status': {
           mouseover: {callback: function(d, self) {
             d3.select(this)
@@ -54,7 +48,6 @@
           }}
         },
         '.rel-label': {
-          click: 'relationClick',
           mousemove: 'mousemove'
         },
         '.topology .crosshatch-background rect:first-child': {
@@ -63,22 +56,15 @@
            * relation.
            */
           click: {callback: function(d, self) {
-            var container = self.get('container');
+            var container = self.get('container'),
+                topo = self.get('component');
             container.all('.environment-menu.active').removeClass('active');
             self.service_click_actions.toggleControlPanel(null, self);
-            self.cancelRelationBuild();
-            self.hideSubordinateRelations();
+            topo.fire('cancelRelationBuild');
+            topo.fire('hideSubordinateRelations');
           }},
           mousemove: 'mousemove'
         },
-        '.dragline': {
-          /** The user clicked while the dragline was active. */
-          click: {callback: function(d, self) {
-            // It was technically the dragline that was clicked, but the
-            // intent was to click on the background, so...
-            self.backgroundClicked();
-          }}
-        },
         '.graph-list-picker .picker-button': {
           click: 'showGraphListPicker'
         },
@@ -86,26 +72,13 @@
           click: 'hideGraphListPicker'
         },
         // Menu/Controls
-        '.add-relation': {
-          /** The user clicked on the "Build Relation" menu item. */
-          click: {
-            callback: function(data, context) {
-              var box = context.get('active_service'),
-                  service = context.serviceForBox(box),
-                  origin = context.get('active_context');
-              context.addRelationDragStart(box, context);
-              context.service_click_actions
-                .toggleControlPanel(box, context, origin);
-              context.service_click_actions.addRelationStart(
-                  box, context, origin);
-            }}
-        },
         '.view-service': {
           /** The user clicked on the "View" menu item. */
           click: {callback: function(data, context) {
             // Get the service element
-            var box = context.get('active_service'),
-                service = context.serviceForBox(box);
+            var topo = context.get('component');
+            var box = topo.get('active_service');
+            var service = topo.serviceForBox(box);
             context.service_click_actions
               .toggleControlPanel(box, context);
             context.service_click_actions
@@ -116,8 +89,9 @@
           /** The user clicked on the "Destroy" menu item. */
           click: {callback: function(data, context) {
             // Get the service element
-            var box = context.get('active_service'),
-                service = context.serviceForBox(box);
+            var topo = context.get('component');
+            var box = topo.get('active_service');
+            var service = topo.serviceForBox(box);
             context.service_click_actions
               .toggleControlPanel(box, context);
             context.service_click_actions
@@ -125,38 +99,18 @@
           }}
         }
       },
-      d3: {
-        '.service': {
-          'mousedown.addrel': {callback: function(d, context) {
-            var evt = d3.event;
-            context.longClickTimer = Y.later(750, this, function(d, e) {
-              // Provide some leeway for accidental dragging.
-              if ((Math.abs(d.x - d.oldX) + Math.abs(d.y - d.oldY)) /
-                  2 > 5) {
-                return;
-              }
-
-              // Sometimes mouseover is fired after the mousedown, so ensure
-              // we have the correct event in d3.event for d3.mouse().
-              d3.event = e;
-
-              // Start the process of adding a relation
-              context.addRelationDragStart(d, context);
-            }, [d, evt], false);
-          }},
-          'mouseup.addrel': {callback: function(d, context) {
-            // Cancel the long-click timer if it exists.
-            if (context.longClickTimer) {
-              context.longClickTimer.cancel();
-            }
-          }}
-        }
-      },
       yui: {
         windowresize: {
           callback: 'setSizesFromViewport',
           context: 'module'},
-        rendered: 'renderedHandler'
+        rendered: 'renderedHandler',
+        show: 'show',
+        hide: 'hide',
+        fade: 'fade',
+        toggleControlPanel: {callback: function() {
+          this.service_click_actions.toggleControlPanel(null, this);
+        }},
+        rescaled: 'updateServiceMenuLocation'
       }
     },
 
@@ -190,37 +144,11 @@
 
     serviceDblClick: function(d, self) {
       // Just show the service on double-click.
-      var service = self.serviceForBox(d);
+      var topo = self.get('component'),
+          service = topo.serviceForBox(d);
       (self.service_click_actions.show_service)(service, self);
     },
 
-    relationClick: function(d, self) {
-      if (d.scope === 'container') {
-        var subRelDialog = views.createModalPanel(
-            'You may not remove a subordinate relation.',
-            '#rmsubrelation-modal-panel');
-        subRelDialog.addButton(
-            { value: 'Cancel',
-              section: Y.WidgetStdMod.FOOTER,
-              /**
-               * @method action Hides the dialog on click.
-               * @param {object} e The click event.
-               * @return {undefined} nothing.
-               */
-              action: function(e) {
-                e.preventDefault();
-                subRelDialog.hide();
-                subRelDialog.destroy();
-              },
-              classNames: ['btn']
-            });
-        subRelDialog.get('boundingBox').all('.yui3-button')
-                .removeClass('yui3-button');
-      } else {
-        self.removeRelationConfirm(d, this, self);
-      }
-    },
-
     /**
      * If the mouse moves and we are adding a relation, then the dragline
      * needs to be updated.
@@ -250,7 +178,6 @@
       var topo = this.get('component'),
           vis = topo.vis,
           db = topo.get('db'),
-          relations = db.relations.toArray(),
           services = db.services.map(views.toBoundingBox);
 
       this.services = services;
@@ -274,7 +201,7 @@
                   right: 0.086758});
         this.service_boxes[service.id] = service;
       }, this);
-      this.rel_pairs = this.processRelations(relations);
+      topo.service_boxes = this.service_boxes;
 
       // Nodes are mapped by modelId tuples.
       this.node = vis.selectAll('.service')
@@ -319,8 +246,8 @@
                 self.service_click_actions.toggleControlPanel(null, self);
               })
             .on('drag', function(d, i) {
-                if (self.buildingRelation) {
-                  self.addRelationDrag(d, this);
+                if (topo.buildingRelation) {
+                  topo.fire('addRelationDrag', { box: d });
                 } else {
                   if (self.longClickTimer) {
                     self.longClickTimer.cancel();
@@ -332,57 +259,25 @@
                   d3.select(this).attr('transform', function(d, i) {
                     return d.translateStr();
                   });
-                  if (self.get('active_service') === d) {
+                  if (topo.get('active_service') === d) {
                     self.updateServiceMenuLocation();
                   }
 
                   // Clear any state while dragging.
                   self.get('container').all('.environment-menu.active')
                     .removeClass('active');
-                  self.cancelRelationBuild();
+                  topo.fire('cancelRelationBuild');
 
                   // Update relation lines for just this service.
-                  updateLinkEndpoints(d);
+                  topo.fire('serviceMoved', { service: d });
                 }
               })
             .on('dragend', function(d, i) {
-                if (self.buildingRelation) {
-                  self.addRelationDragEnd();
+                if (topo.buildingRelation) {
+                  topo.fire('addRelationDragEnd');
                 }
               });
 
-      /**
-       * Update relation line endpoints for a given service.
-       *
-       * @method updateLinkEndpoints
-       * @param {Object} service The service module that has been moved.
-       */
-      function updateLinkEndpoints(service) {
-        Y.each(Y.Array.filter(self.rel_pairs, function(relation) {
-          return relation.source() === service ||
-              relation.target() === service;
-        }), function(relation) {
-          var rel_group = d3.select('#' + relation.id),
-                  connectors = relation.source()
-                    .getConnectorPair(relation.target()),
-                  s = connectors[0],
-                  t = connectors[1];
-          rel_group.select('line')
-                .attr('x1', s[0])
-                .attr('y1', s[1])
-                .attr('x2', t[0])
-                .attr('y2', t[1]);
-          rel_group.select('.rel-label')
-                .attr('transform', function(d) {
-                return 'translate(' +
-                    [Math.max(s[0], t[0]) -
-                         Math.abs((s[0] - t[0]) / 2),
-                         Math.max(s[1], t[1]) -
-                         Math.abs((s[1] - t[1]) / 2)] + ')';
-              });
-        });
-      }
-
       // Generate a node for each service, draw it as a rect with
       // labels for service and charm.
       var node = this.node;
@@ -415,111 +310,12 @@
       // Exit
       node.exit()
           .remove();
-
-      function updateLinks() {
-        // Enter.
-        var g = self.drawRelationGroup(),
-                link = g.selectAll('line.relation');
-
-        // Update (+ enter selection).
-        link.each(self.drawRelation);
-
-        // Exit
-        g.exit().remove();
-      }
-
-      // Draw or schedule redraw of links.
-      updateLinks();
-
-    },
-
-    /*
-     * Draw a new relation link with label and controls.
-     */
-    drawRelationGroup: function() {
-      // Add a labelgroup.
-      var self = this,
-          vis = this.get('component').vis,
-          g = vis.selectAll('g.rel-group')
-                 .data(self.rel_pairs, function(r) {
-                   return r.modelIds();
-                 });
-
-      var enter = g.enter();
-
-      enter.insert('g', 'g.service')
-              .attr('id', function(d) {
-            return d.id;
-          })
-              .attr('class', function(d) {
-                // Mark the rel-group as a subordinate relation if need be.
-                return (d.scope === 'container' ?
-                    'subordinate-rel-group ' : '') +
-                    'rel-group';
-              })
-              .append('svg:line', 'g.service')
-              .attr('class', function(d) {
-                // Style relation lines differently depending on status.
-                return (d.pending ? 'pending-relation ' : '') +
-                    (d.scope === 'container' ? 'subordinate-relation ' : '') +
-                    'relation';
-              });
-
-      g.selectAll('.rel-label').remove();
-      g.selectAll('text').remove();
-      g.selectAll('rect').remove();
-      var label = g.append('g')
-              .attr('class', 'rel-label')
-              .attr('transform', function(d) {
-                // XXX: This has to happen on update, not enter
-                var connectors = d.source().getConnectorPair(d.target()),
-                    s = connectors[0],
-                    t = connectors[1];
-                return 'translate(' +
-                    [Math.max(s[0], t[0]) -
-                     Math.abs((s[0] - t[0]) / 2),
-                     Math.max(s[1], t[1]) -
-                     Math.abs((s[1] - t[1]) / 2)] + ')';
-              });
-      label.append('text')
-              .append('tspan')
-              .text(function(d) {return d.display_name; });
-      label.insert('rect', 'text')
-              .attr('width', function(d) {
-            return d.display_name.length * 10 + 10;
-          })
-              .attr('height', 20)
-              .attr('x', function() {
-                return -parseInt(d3.select(this).attr('width'), 10) / 2;
-              })
-              .attr('y', -10)
-              .attr('rx', 10)
-              .attr('ry', 10);
-
-      return g;
-    },
-
-    /*
-         * Draw a relation between services.
-         */
-    drawRelation: function(relation) {
-      var connectors = relation.source()
-                .getConnectorPair(relation.target()),
-              s = connectors[0],
-              t = connectors[1],
-              link = d3.select(this);
-
-      link
-                .attr('x1', s[0])
-                .attr('y1', s[1])
-                .attr('x2', t[0])
-                .attr('y2', t[1]);
-      return link;
     },
 
     // Called to draw a service in the 'update' phase
     drawService: function(node) {
       var self = this,
+              topo = this.get('component'),
               service_scale = this.service_scale,
               service_scale_width = this.service_scale_width,
               service_scale_height = this.service_scale_height;
@@ -573,10 +369,7 @@
       sub_relation.append('text').append('tspan')
             .attr('class', 'sub-rel-count')
             .attr('x', 64)
-            .attr('y', 47 * 0.8)
-            .text(function(d) {
-                return self.subordinateRelationsForService(d).length;
-              });
+            .attr('y', 47 * 0.8);
       // Draw non-subordinate services services
       node.filter(function(d) {
         return !d.subordinate;
@@ -740,78 +533,28 @@
 
     },
 
-    processRelation: function(r) {
-      var self = this,
-              endpoints = r.get('endpoints'),
-              rel_services = [];
-
-      Y.each(endpoints, function(ep) {
-        rel_services.push([ep[1].name, self.service_boxes[ep[0]]]);
-      });
-      return rel_services;
-    },
-
-    processRelations: function(rels) {
-      var self = this,
-              pairs = [];
-      Y.each(rels, function(rel) {
-        var pair = self.processRelation(rel);
-
-        // skip peer for now
-        if (pair.length === 2) {
-          var bpair = views.BoxPair()
-                                 .model(rel)
-                                 .source(pair[0][1])
-                                 .target(pair[1][1]);
-          // Copy the relation type to the box.
-          if (bpair.display_name === undefined) {
-            bpair.display_name = pair[0][0];
-          }
-          pairs.push(bpair);
-        }
-      });
-      return pairs;
-    },
-
-    /*
-         * Utility function to get subordinate relations for a service.
-         */
-    subordinateRelationsForService: function(service) {
-      return this.rel_pairs.filter(function(p) {
-        return p.modelIds().indexOf(service.modelId()) !== -1 &&
-            p.scope === 'container';
-      });
-    },
-    /*
-         * Utility method to get a service object from the DB
-         * given a BoundingBox.
-         */
-    serviceForBox: function(boundingBox) {
-      var db = this.get('component').get('db');
-      return db.services.getById(boundingBox.id);
-    },
-
 
     /*
          * Show/hide/fade selection.
          */
-    show: function(selection) {
+    show: function(evt) {
+      var selection = evt.selection;
       selection.attr('opacity', '1.0')
                 .style('display', 'block');
-      return selection;
     },
 
-    hide: function(selection) {
+    hide: function(evt) {
+      var selection = evt.selection;
       selection.attr('opacity', '0')
             .style('display', 'none');
-      return selection;
     },
 
-    fade: function(selection, alpha) {
+    fade: function(evt) {
+      var selection = evt.selection,
+          alpha = evt.alpha;
       selection.transition()
             .duration(400)
             .attr('opacity', alpha !== undefined && alpha || '0.2');
-      return selection;
     },
 
     /*
@@ -844,153 +587,6 @@
       return this;
     },
 
-    /*
-         * Event handler for the add relation button.
-         */
-    addRelation: function(evt) {
-      var curr_action = this.get('currentServiceClickAction'),
-              container = this.get('container');
-      if (curr_action === 'show_service') {
-        this.set('currentServiceClickAction', 'addRelationStart');
-      } else if (curr_action === 'addRelationStart' ||
-              curr_action === 'ambiguousAddRelationCheck') {
-        this.set('currentServiceClickAction', 'toggleControlPanel');
-      } // Otherwise do nothing.
-    },
-
-    addRelationDragStart: function(d, context) {
-      // Create a pending drag-line.
-      var vis = this.get('component').vis,
-          dragline = vis.append('line')
-                        .attr('class',
-                              'relation pending-relation dragline dragging'),
-          self = this;
-
-      // Start the line between the cursor and the nearest connector
-      // point on the service.
-      var mouse = d3.mouse(Y.one('.topology svg').getDOMNode());
-      self.cursorBox = new views.BoundingBox();
-      self.cursorBox.pos = {x: mouse[0], y: mouse[1], w: 0, h: 0};
-      var point = self.cursorBox.getConnectorPair(d);
-      dragline.attr('x1', point[0][0])
-              .attr('y1', point[0][1])
-              .attr('x2', point[1][0])
-              .attr('y2', point[1][1]);
-      self.dragline = dragline;
-
-      // Start the add-relation process.
-      context.service_click_actions
-            .addRelationStart(d, self, context);
-    },
-
-    addRelationDrag: function(d, context) {
-      // Rubberband our potential relation line if we're not currently
-      // hovering over a potential drop-point.
-      if (!this.get('potential_drop_point_service')) {
-        // Create a BoundingBox for our cursor.
-        this.cursorBox.pos = {x: d3.event.x, y: d3.event.y, w: 0, h: 0};
-
-        // Draw the relation line from the connector point nearest the
-        // cursor to the cursor itself.
-        var connectors = this.cursorBox.getConnectorPair(d),
-                s = connectors[1];
-        this.dragline.attr('x1', s[0])
-              .attr('y1', s[1])
-              .attr('x2', d3.event.x)
-              .attr('y2', d3.event.y);
-      }
-    },
-
-    addRelationDragEnd: function() {
-      // Get the line, the endpoint service, and the target <rect>.
-      var self = this;
-      var rect = self.get('potential_drop_point_rect');
-      var endpoint = self.get('potential_drop_point_service');
-
-      self.buildingRelation = false;
-      self.cursorBox = null;
-
-      // If we landed on a rect, add relation, otherwise, cancel.
-      if (rect) {
-        self.service_click_actions
-            .ambiguousAddRelationCheck(endpoint, self, rect);
-      } else {
-        // TODO clean up, abstract
-        self.cancelRelationBuild();
-        self.addRelation(); // Will clear the state.
-      }
-    },
-    removeRelation: function(d, context, view, confirmButton) {
-      var env = this.get('component').get('env'),
-              endpoints = d.endpoints,
-              relationElement = Y.one(context.parentNode).one('.relation');
-      utils.addSVGClass(relationElement, 'to-remove pending-relation');
-      env.remove_relation(
-          endpoints[0][0] + ':' + endpoints[0][1].name,
-          endpoints[1][0] + ':' + endpoints[1][1].name,
-          Y.bind(this._removeRelationCallback, this, view,
-          relationElement, d.relation_id, confirmButton));
-    },
-
-    _removeRelationCallback: function(view,
-            relationElement, relationId, confirmButton, ev) {
-      var db = this.get('component').get('db'),
-          service = this.get('model');
-      if (ev.err) {
-        db.notifications.add(
-            new models.Notification({
-              title: 'Error deleting relation',
-              message: 'Relation ' + ev.endpoint_a + ' to ' + ev.endpoint_b,
-              level: 'error'
-            })
-        );
-        utils.removeSVGClass(this.relationElement,
-            'to-remove pending-relation');
-      } else {
-        // Remove the relation from the DB.
-        db.relations.remove(db.relations.getById(relationId));
-        // Redraw the graph and reattach events.
-        db.fire('update');
-      }
-      view.get('rmrelation_dialog').hide();
-      view.get('rmrelation_dialog').destroy();
-      confirmButton.set('disabled', false);
-    },
-
-    removeRelationConfirm: function(d, context, view) {
-      // Destroy the dialog if it already exists to prevent cluttering
-      // up the DOM.
-      if (!Y.Lang.isUndefined(view.get('rmrelation_dialog'))) {
-        view.get('rmrelation_dialog').destroy();
-      }
-      view.set('rmrelation_dialog', views.createModalPanel(
-          'Are you sure you want to remove this relation? ' +
-              'This cannot be undone.',
-          '#rmrelation-modal-panel',
-          'Remove Relation',
-          Y.bind(function(ev) {
-            ev.preventDefault();
-            var confirmButton = ev.target;
-            confirmButton.set('disabled', true);
-            view.removeRelation(d, context, view, confirmButton);
-          },
-          this)));
-    },
-
-    cancelRelationBuild: function() {
-      var vis = this.get('component').vis;
-      if (this.dragline) {
-        // Get rid of our drag line
-        this.dragline.remove();
-        this.dragline = null;
-      }
-      this.clickAddRelation = null;
-      this.set('currentServiceClickAction', 'toggleControlPanel');
-      this.buildingRelation = false;
-      this.show(vis.selectAll('.service'))
-                  .classed('selectable-service', false);
-    },
-
     /**
      * The user clicked on the environment view background.
      *
@@ -1001,64 +597,8 @@
      * @return {undefined} Side effects only.
      */
     backgroundClicked: function() {
-      if (this.clickAddRelation) {
-        this.cancelRelationBuild();
-      }
-    },
-
-    /**
-     * An "add relation" action has been initiated by the user.
-     *
-     * @method startRelation
-     * @param {object} service The service that is the source of the
-     *  relation.
-     * @return {undefined} Side effects only.
-     */
-    startRelation: function(service) {
-      // Set flags on the view that indicate we are building a relation.
-      var vis = this.get('component').vis;
-
-      this.buildingRelation = true;
-      this.clickAddRelation = true;
-
-      this.show(vis.selectAll('.service'));
-
-      var db = this.get('component').get('db'),
-          getServiceEndpoints = this.get('component')
-                                    .get('getServiceEndpoints'),
-          endpoints = models.getEndpoints(
-          service, getServiceEndpoints(), db),
-          // Transform endpoints into a list of relatable services (to the
-          // service).
-          possible_relations = Y.Array.map(
-              Y.Array.flatten(Y.Object.values(endpoints)),
-              function(ep) {return ep.service;}),
-              invalidRelationTargets = {};
-
-      // Iterate services and invert the possibles list.
-      db.services.each(function(s) {
-        if (Y.Array.indexOf(possible_relations,
-            s.get('id')) === -1) {
-          invalidRelationTargets[s.get('id')] = true;
-        }
-      });
-
-      // Fade elements to which we can't relate.
-      // Rather than two loops this marks
-      // all services as selectable and then
-      // removes the invalid ones.
-      this.fade(vis.selectAll('.service')
-              .classed('selectable-service', true)
-              .filter(function(d) {
-                return (d.id in invalidRelationTargets &&
-                          d.id !== service.id);
-              }))
-              .classed('selectable-service', false);
-
-      // Store possible endpoints.
-      this.set('addRelationStart_possibleEndpoints', endpoints);
-      // Set click action.
-      this.set('currentServiceClickAction', 'ambiguousAddRelationCheck');
+      var topo = this.get('component');
+      topo.fire('clearState');
     },
 
     /*
@@ -1081,33 +621,6 @@
       picker.one('.picker-expanded').removeClass('active');
     },
 
-    /**
-     * Show subordinate relations for a service.
-     *
-     * @method showSubordinateRelations
-     * @param {Object} subordinate The sub-rel-block g element in the form
-     * of a DOM node.
-     * @return {undefined} nothing.
-     */
-    showSubordinateRelations: function(subordinate) {
-      this.keepSubRelationsVisible = true;
-      utils.addSVGClass(Y.one(subordinate).one('.sub-rel-count'), 'active');
-    },
-
-    /**
-     * Hide subordinate relations.
-     *
-     * @method hideSubordinateRelations
-     * @return {undefined} nothing.
-     */
-    hideSubordinateRelations: function() {
-      var container = this.get('container');
-      utils.removeSVGClass('.subordinate-rel-group', 'active');
-      this.keepSubRelationsVisible = false;
-      utils.removeSVGClass(container.one('.sub-rel-count.active'),
-          'active');
-    },
-
     /*
          * Set the visualization size based on the viewport
          */
@@ -1163,7 +676,7 @@
       var topo = this.get('component'),
           container = this.get('container'),
           cp = container.one('.environment-menu.active'),
-          service = this.get('active_service'),
+          service = topo.get('active_service'),
           tr = topo.get('translate'),
           z = topo.get('scale');
 
@@ -1194,100 +707,6 @@
       }
     },
 
-    serviceMouseEnter: function(d, context) {
-      var rect = Y.one(this);
-      // Do not fire if this service isn't selectable.
-      if (!utils.hasSVGClass(rect, 'selectable-service')) {
-        return;
-      }
-
-      // Do not fire unless we're within the service box.
-      var topo = context.get('component'),
-          container = context.get('container'),
-          mouse_coords = d3.mouse(container.one('svg').getDOMNode());
-      if (!d.containsPoint(mouse_coords, topo.zoom)) {
-        return;
-      }
-
-      // Do not fire if we're on the same service.
-      if (d === context.get('addRelationStart_service')) {
-        return;
-      }
-
-      context.set('potential_drop_point_service', d);
-      context.set('potential_drop_point_rect', rect);
-      utils.addSVGClass(rect, 'hover');
-
-      // If we have an active dragline, stop redrawing it on mousemove
-      // and draw the line between the two nearest connector points of
-      // the two services.
-      if (context.dragline) {
-        var connectors = d.getConnectorPair(
-            context.get('addRelationStart_service')),
-            s = connectors[0],
-            t = connectors[1];
-        context.dragline.attr('x1', t[0])
-        .attr('y1', t[1])
-        .attr('x2', s[0])
-        .attr('y2', s[1])
-        .attr('class', 'relation pending-relation dragline');
-      }
-    },
-
-    serviceMouseLeave: function(d, self) {
-      // Do not fire if we aren't looking for a relation endpoint.
-      if (!self.get('potential_drop_point_rect')) {
-        return;
-      }
-
-      // Do not fire if we're within the service box.
-      var topo = this.get('component'),
-          container = self.get('container'),
-          mouse_coords = d3.mouse(container.one('svg').getDOMNode());
-      if (d.containsPoint(mouse_coords, topo.zoom)) {
-        return;
-      }
-      var rect = Y.one(this).one('.service-border');
-      self.set('potential_drop_point_service', null);
-      self.set('potential_drop_point_rect', null);
-      utils.removeSVGClass(rect, 'hover');
-
-      if (self.dragline) {
-        self.dragline.attr('class',
-                         'relation pending-relation dragline dragging');
-      }
-    },
-
-    subRelBlockMouseEnter: function(d, self) {
-      // Add an 'active' class to all of the subordinate relations
-      // belonging to this service.
-      self.subordinateRelationsForService(d)
-    .forEach(function(p) {
-            utils.addSVGClass('#' + p.id, 'active');
-          });
-    },
-
-    subRelBlockMouseLeave: function(d, self) {
-      // Remove 'active' class from all subordinate relations.
-      if (!self.keepSubRelationsVisible) {
-        utils.removeSVGClass('.subordinate-rel-group', 'active');
-      }
-    },
-
-    /**
-     * Toggle the visibility of subordinate relations for visibility
-     * or removal.
-     * @param {object} d The data-bound object (the subordinate).
-     * @param {object} self The view.
-     **/
-    subRelBlockClick: function(d, self) {
-      if (self.keepSubRelationsVisible) {
-        self.hideSubordinateRelations();
-      } else {
-        self.showSubordinateRelations(this);
-      }
-    },
-
     /*
          * Actions to be called on clicking a service.
          */
@@ -1297,15 +716,16 @@
            */
       toggleControlPanel: function(m, view, context) {
         var container = view.get('container'),
+            topo = view.get('component'),
                 cp = container.one('#service-menu');
 
         if (cp.hasClass('active') || !m) {
           cp.removeClass('active');
-          view.set('active_service', null);
-          view.set('active_context', null);
+          topo.set('active_service', null);
+          topo.set('active_context', null);
         } else {
-          view.set('active_service', m);
-          view.set('active_context', context);
+          topo.set('active_service', m);
+          topo.set('active_context', context);
           cp.addClass('active');
           view.updateServiceMenuLocation();
         }
@@ -1378,183 +798,9 @@
           db.fire('update');
         }
         btn.set('disabled', false);
-      },
-
-
-      /*
-           * Fired when clicking the first service in the add relation
-           * flow.
-           */
-      addRelationStart: function(m, view, context) {
-        var service = view.serviceForBox(m);
-        view.startRelation(service);
-        // Store start service in attrs.
-        view.set('addRelationStart_service', m);
-      },
-
-      /*
-           * Test if the pending relation is ambiguous.  Display a menu if so,
-           * create the relation if not.
-           */
-      ambiguousAddRelationCheck: function(m, view, context) {
-        var endpoints = view.get(
-            'addRelationStart_possibleEndpoints')[m.id],
-            container = view.get('container'),
-            topo = view.get('component');
-
-        if (endpoints && endpoints.length === 1) {
-          // Create a relation with the only available endpoint.
-          var ep = endpoints[0],
-                  endpoints_item = [
-                    [ep[0].service, {
-                      name: ep[0].name,
-                      role: 'server' }],
-                    [ep[1].service, {
-                      name: ep[1].name,
-                      role: 'client' }]];
-          view.service_click_actions
-                .addRelationEnd(endpoints_item, view, context);
-          return;
-        }
-
-        // Sort the endpoints alphabetically by relation name.
-        endpoints = endpoints.sort(function(a, b) {
-          return a[0].name + a[1].name < b[0].name + b[1].name;
-        });
-
-        // Stop rubberbanding on mousemove.
-        view.clickAddRelation = null;
-
-        // Display menu with available endpoints.
-        var menu = container.one('#ambiguous-relation-menu');
-        if (menu.one('.menu')) {
-          menu.one('.menu').remove(true);
-        }
-
-        menu.append(Templates
-                .ambiguousRelationList({endpoints: endpoints}));
-
-        // For each endpoint choice, bind an an event to 'click' to
-        // add the specified relation.
-        menu.all('li').on('click', function(evt) {
-          if (evt.currentTarget.hasClass('cancel')) {
-            return;
-          }
-          var el = evt.currentTarget,
-                  endpoints_item = [
-                    [el.getData('startservice'), {
-                      name: el.getData('startname'),
-                      role: 'server' }],
-                    [el.getData('endservice'), {
-                      name: el.getData('endname'),
-                      role: 'client' }]];
-          menu.removeClass('active');
-          view.service_click_actions
-                .addRelationEnd(endpoints_item, view, context);
-        });
-
-        // Add a cancel item.
-        menu.one('.cancel').on('click', function(evt) {
-          menu.removeClass('active');
-          view.cancelRelationBuild();
-        });
-
-        // Display the menu at the service endpoint.
-        var tr = topo.zoom.translate(),
-                z = topo.zoom.scale();
-        menu.setStyle('top', m.y * z + tr[1]);
-        menu.setStyle('left', m.x * z + m.w * z + tr[0]);
-        menu.addClass('active');
-        view.set('active_service', m);
-        view.set('active_context', context);
-        view.updateServiceMenuLocation();
-      },
-
-      /*
-       * Fired when clicking the second service is clicked in the
-       * add relation flow.
-       *
-       * :param endpoints: array of two endpoints, each in the form
-       *   ['service name', {
-       *     name: 'endpoint type',
-       *     role: 'client or server'
-       *   }]
-       */
-      addRelationEnd: function(endpoints, view, context) {
-        // Redisplay all services
-        view.cancelRelationBuild();
-
-        // Get the vis, and links, build the new relation.
-        var vis = view.get('component').vis,
-            env = view.get('component').get('env'),
-            db = view.get('component').get('db'),
-            source = view.get('addRelationStart_service'),
-            relation_id = 'pending:' + endpoints[0][0] + endpoints[1][0];
-
-        if (endpoints[0][0] === endpoints[1][0]) {
-          view.set('currentServiceClickAction', 'toggleControlPanel');
-          return;
-        }
-
-        // Create a pending relation in the database between the
-        // two services.
-        db.relations.create({
-          relation_id: relation_id,
-          display_name: 'pending',
-          endpoints: endpoints,
-          pending: true
-        });
-
-        // Firing the update event on the db will properly redraw the
-        // graph and reattach events.
-        //db.fire('update');
-        view.get('component').bindAllD3Events();
-        view.update();
-
-        // Fire event to add relation in juju.
-        // This needs to specify interface in the future.
-        env.add_relation(
-            endpoints[0][0] + ':' + endpoints[0][1].name,
-            endpoints[1][0] + ':' + endpoints[1][1].name,
-            Y.bind(this._addRelationCallback, this, view, relation_id)
-        );
-        view.set('currentServiceClickAction', 'toggleControlPanel');
-      },
-
-      _addRelationCallback: function(view, relation_id, ev) {
-        var db = view.get('component').get('db');
-        // Remove our pending relation from the DB, error or no.
-        db.relations.remove(
-            db.relations.getById(relation_id));
-        if (ev.err) {
-          db.notifications.add(
-              new models.Notification({
-                title: 'Error adding relation',
-                message: 'Relation ' + ev.endpoint_a +
-                    ' to ' + ev.endpoint_b,
-                level: 'error'
-              })
-          );
-        } else {
-          // Create a relation in the database between the two services.
-          var result = ev.result,
-                  endpoints = Y.Array.map(result.endpoints, function(item) {
-                    var id = Y.Object.keys(item)[0];
-                    return [id, item[id]];
-                  });
-          db.relations.create({
-            relation_id: ev.result.id,
-            type: result['interface'],
-            endpoints: endpoints,
-            pending: false,
-            scope: result.scope,
-            // endpoints[1][1].name should be the same
-            display_name: endpoints[0][1].name
-          });
-        }
-        // Redraw the graph and reattach events.
-        db.fire('update');
       }
+
+
     }
   }, {
     ATTRS: {}

=== modified file 'app/views/topology/relation.js'
--- app/views/topology/relation.js	2012-12-11 03:58:03 +0000
+++ app/views/topology/relation.js	2012-12-21 19:34:22 +0000
@@ -3,7 +3,9 @@
 YUI.add('juju-topology-relation', function(Y) {
   var views = Y.namespace('juju.views'),
       models = Y.namespace('juju.models'),
-      d3ns = Y.namespace('d3');
+      utils = Y.namespace('juju.views.utils'),
+      d3ns = Y.namespace('d3'),
+      Templates = views.Templates;
 
   /**
    * @module topology-relations
@@ -11,8 +13,85 @@
    * @namespace views
    **/
   var RelationModule = Y.Base.create('RelationModule', d3ns.Module, [], {
+
+    events: {
+      scene: {
+        '.service': {
+          mouseenter: 'serviceMouseEnter',
+          mouseleave: 'serviceMouseLeave'
+        },
+        '.sub-rel-block': {
+          mouseenter: 'subRelBlockMouseEnter',
+          mouseleave: 'subRelBlockMouseLeave',
+          click: 'subRelBlockClick'
+        },
+        '.rel-label': {
+          click: 'relationClick'
+        },
+        '.dragline': {
+          /** The user clicked while the dragline was active. */
+          click: {callback: function(d, self) {
+            // It was technically the dragline that was clicked, but the
+            // intent was to click on the background, so...
+            self.backgroundClicked();
+          }}
+        },
+        '.add-relation': {
+          /** The user clicked on the "Build Relation" menu item. */
+          click: {
+            callback: function(data, context) {
+              var topo = context.get('component');
+              var box = topo.get('active_service');
+              var service = topo.serviceForBox(box);
+              var origin = topo.get('active_context');
+              context.addRelationDragStart(box, context);
+              topo.fire('toggleControlPanel');
+              context.addRelationStart(box, context, origin);
+            }}
+        }
+      },
+      d3: {
+        '.service': {
+          'mousedown.addrel': {callback: function(d, context) {
+            var evt = d3.event;
+            context.longClickTimer = Y.later(750, this, function(d, e) {
+              // Provide some leeway for accidental dragging.
+              if ((Math.abs(d.x - d.oldX) + Math.abs(d.y - d.oldY)) /
+                  2 > 5) {
+                return;
+              }
+
+              // Sometimes mouseover is fired after the mousedown, so ensure
+              // we have the correct event in d3.event for d3.mouse().
+              d3.event = e;
+
+              // Start the process of adding a relation
+              context.addRelationDragStart(d, context);
+            }, [d, evt], false);
+          }},
+          'mouseup.addrel': {callback: function(d, context) {
+            // Cancel the long-click timer if it exists.
+            if (context.longClickTimer) {
+              context.longClickTimer.cancel();
+            }
+          }}
+        }
+      },
+      yui: {
+        rendered: {callback: 'renderedHandler'},
+        clearState: {callback: 'cancelRelationBuild'},
+        serviceMoved: {callback: 'updateLinkEndpoints'},
+        servicesRendered: {callback: 'updateLinks'},
+        cancelRelationBuild: {callback: 'cancelRelationBuild'},
+        addRelationDragStart: {callback: 'addRelationDragStart'},
+        addRelationDrag: {callback: 'addRelationDrag'},
+        addRelationDragEnd: {callback: 'addRelationDragEnd'}
+      }
+    },
+
     initializer: function(options) {
       RelationModule.superclass.constructor.apply(this, arguments);
+      this.relPairs = [];
     },
 
     render: function() {
@@ -22,7 +101,742 @@
 
     update: function() {
       RelationModule.superclass.update.apply(this, arguments);
+
+      var topo = this.get('component');
+      var db = topo.get('db');
+      var relations = db.relations.toArray();
+      this.relPairs = this.processRelations(relations);
+      topo.relPairs = this.relPairs;
+      this.updateLinks();
+      this.updateSubordinateRelationsCount();
+
       return this;
+    },
+
+    renderedHandler: function() {
+      this.update();
+    },
+
+    processRelation: function(r) {
+      var self = this;
+      var topo = self.get('component');
+      var endpoints = r.get('endpoints');
+      var rel_services = [];
+
+      Y.each(endpoints, function(ep) {
+        rel_services.push([ep[1].name, topo.service_boxes[ep[0]]]);
+      });
+      return rel_services;
+    },
+
+    processRelations: function(rels) {
+      var self = this;
+      var pairs = [];
+      Y.each(rels, function(rel) {
+        var pair = self.processRelation(rel);
+
+        // skip peer for now
+        if (pair.length === 2) {
+          var bpair = views.BoxPair()
+                                 .model(rel)
+                                 .source(pair[0][1])
+                                 .target(pair[1][1]);
+          // Copy the relation type to the box.
+          if (bpair.display_name === undefined) {
+            bpair.display_name = pair[0][0];
+          }
+          pairs.push(bpair);
+        }
+      });
+      return pairs;
+    },
+
+    updateLinks: function() {
+      // Enter.
+      var g = this.drawRelationGroup();
+      var link = g.selectAll('line.relation');
+
+      // Update (+ enter selection).
+      link.each(this.drawRelation);
+
+      // Exit
+      g.exit().remove();
+    },
+
+    /**
+     * Update relation line endpoints for a given service.
+     *
+     * @method updateLinkEndpoints
+     * @param {Object} service The service module that has been moved.
+     */
+    updateLinkEndpoints: function(evt) {
+      var self = this;
+      var service = evt.service;
+      Y.each(Y.Array.filter(self.relPairs, function(relation) {
+        return relation.source() === service ||
+            relation.target() === service;
+      }), function(relation) {
+        var rel_group = d3.select('#' + relation.id);
+        var connectors = relation.source()
+                  .getConnectorPair(relation.target());
+        var s = connectors[0];
+        var t = connectors[1];
+        rel_group.select('line')
+              .attr('x1', s[0])
+              .attr('y1', s[1])
+              .attr('x2', t[0])
+              .attr('y2', t[1]);
+        rel_group.select('.rel-label')
+              .attr('transform', function(d) {
+              return 'translate(' +
+                  [Math.max(s[0], t[0]) -
+                       Math.abs((s[0] - t[0]) / 2),
+                       Math.max(s[1], t[1]) -
+                       Math.abs((s[1] - t[1]) / 2)] + ')';
+            });
+      });
+    },
+
+    drawRelationGroup: function() {
+      // Add a labelgroup.
+      var self = this;
+      var vis = this.get('component').vis;
+      var g = vis.selectAll('g.rel-group')
+                 .data(self.relPairs, function(r) {
+            return r.modelIds();
+          });
+
+      var enter = g.enter();
+
+      enter.insert('g', 'g.service')
+              .attr('id', function(d) {
+            return d.id;
+          })
+              .attr('class', function(d) {
+                // Mark the rel-group as a subordinate relation if need be.
+                return (d.scope === 'container' ?
+                    'subordinate-rel-group ' : '') +
+                    'rel-group';
+              })
+              .append('svg:line', 'g.service')
+              .attr('class', function(d) {
+                // Style relation lines differently depending on status.
+                return (d.pending ? 'pending-relation ' : '') +
+                    (d.scope === 'container' ? 'subordinate-relation ' : '') +
+                    'relation';
+              });
+
+      g.selectAll('.rel-label').remove();
+      g.selectAll('text').remove();
+      g.selectAll('rect').remove();
+      var label = g.append('g')
+              .attr('class', 'rel-label')
+              .attr('transform', function(d) {
+                // XXX: This has to happen on update, not enter
+                var connectors = d.source().getConnectorPair(d.target());
+                var s = connectors[0];
+                var t = connectors[1];
+                return 'translate(' +
+                    [Math.max(s[0], t[0]) -
+                     Math.abs((s[0] - t[0]) / 2),
+                     Math.max(s[1], t[1]) -
+                     Math.abs((s[1] - t[1]) / 2)] + ')';
+              });
+      label.append('text')
+              .append('tspan')
+              .text(function(d) {return d.display_name; });
+      label.insert('rect', 'text')
+              .attr('width', function(d) {
+            return d.display_name.length * 10 + 10;
+          })
+              .attr('height', 20)
+              .attr('x', function() {
+                return -parseInt(d3.select(this).attr('width'), 10) / 2;
+              })
+              .attr('y', -10)
+              .attr('rx', 10)
+              .attr('ry', 10);
+
+      return g;
+    },
+
+    drawRelation: function(relation) {
+      var connectors = relation.source()
+                .getConnectorPair(relation.target());
+      var s = connectors[0];
+      var t = connectors[1];
+      var link = d3.select(this);
+
+      link
+                .attr('x1', s[0])
+                .attr('y1', s[1])
+                .attr('x2', t[0])
+                .attr('y2', t[1]);
+      return link;
+    },
+
+    updateSubordinateRelationsCount: function() {
+      var topo = this.get('component');
+      var vis = topo.vis;
+      var self = this;
+
+      vis.selectAll('.service')
+        .filter(function(d) {
+            return d.subordinate;
+          })
+        .select('.sub-rel-block tspan')
+        .text(function(d) {
+            return self.subordinateRelationsForService(d).length;
+          });
+    },
+
+    /*
+         * Event handler for the add relation button.
+         */
+    addRelation: function(evt) {
+      var curr_action = this.get('currentServiceClickAction');
+      if (curr_action === 'show_service') {
+        this.set('currentServiceClickAction', 'addRelationStart');
+      } else if (curr_action === 'addRelationStart' ||
+              curr_action === 'ambiguousAddRelationCheck') {
+        this.set('currentServiceClickAction', 'toggleControlPanel');
+      } // Otherwise do nothing.
+    },
+
+    serviceMouseEnter: function(d, context) {
+      var rect = Y.one(this);
+      // Do not fire if this service isn't selectable.
+      if (!utils.hasSVGClass(rect, 'selectable-service')) {
+        return;
+      }
+
+      // Do not fire unless we're within the service box.
+      var topo = context.get('component');
+      var container = context.get('container');
+      var mouse_coords = d3.mouse(container.one('svg').getDOMNode());
+      if (!d.containsPoint(mouse_coords, topo.zoom)) {
+        return;
+      }
+
+      // Do not fire if we're on the same service.
+      if (d === context.get('addRelationStart_service')) {
+        return;
+      }
+
+      context.set('potential_drop_point_service', d);
+      context.set('potential_drop_point_rect', rect);
+      utils.addSVGClass(rect, 'hover');
+
+      // If we have an active dragline, stop redrawing it on mousemove
+      // and draw the line between the two nearest connector points of
+      // the two services.
+      if (context.dragline) {
+        var connectors = d.getConnectorPair(
+            context.get('addRelationStart_service'));
+        var s = connectors[0];
+        var t = connectors[1];
+        context.dragline.attr('x1', t[0])
+        .attr('y1', t[1])
+        .attr('x2', s[0])
+        .attr('y2', s[1])
+        .attr('class', 'relation pending-relation dragline');
+        context.draglineOverService = true;
+      }
+    },
+
+    serviceMouseLeave: function(d, self) {
+      // Do not fire if we aren't looking for a relation endpoint.
+      if (!self.get('potential_drop_point_rect')) {
+        return;
+      }
+
+      // Do not fire if we're within the service box.
+      var topo = self.get('component');
+      var container = self.get('container');
+      var mouse_coords = d3.mouse(container.one('svg').getDOMNode());
+      if (d.containsPoint(mouse_coords, topo.zoom)) {
+        return;
+      }
+      var rect = Y.one(this).one('.service-border');
+      self.set('potential_drop_point_service', null);
+      self.set('potential_drop_point_rect', null);
+      utils.removeSVGClass(rect, 'hover');
+
+      if (self.dragline) {
+        self.dragline.attr('class',
+            'relation pending-relation dragline dragging');
+        self.draglineOverService = false;
+      }
+    },
+
+    addRelationDragStart: function(d, context) {
+      // Create a pending drag-line.
+      var vis = this.get('component').vis;
+      var dragline = vis.append('line')
+                        .attr('class',
+                              'relation pending-relation dragline dragging');
+      var self = this;
+
+      // Start the line between the cursor and the nearest connector
+      // point on the service.
+      var mouse = d3.mouse(Y.one('.topology svg').getDOMNode());
+      self.cursorBox = new views.BoundingBox();
+      self.cursorBox.pos = {x: mouse[0], y: mouse[1], w: 0, h: 0};
+      var point = self.cursorBox.getConnectorPair(d);
+      dragline.attr('x1', point[0][0])
+              .attr('y1', point[0][1])
+              .attr('x2', point[1][0])
+              .attr('y2', point[1][1]);
+      self.dragline = dragline;
+
+      // Start the add-relation process.
+      self.addRelationStart(d, self, context);
+    },
+
+    addRelationDrag: function(evt) {
+      var d = evt.box;
+
+      // Rubberband our potential relation line if we're not currently
+      // hovering over a potential drop-point.
+      if (!this.get('potential_drop_point_service') &&
+          !this.draglineOverService) {
+        // Create a BoundingBox for our cursor.
+        this.cursorBox.pos = {x: d3.event.x, y: d3.event.y, w: 0, h: 0};
+
+        // Draw the relation line from the connector point nearest the
+        // cursor to the cursor itself.
+        var connectors = this.cursorBox.getConnectorPair(d),
+                s = connectors[1];
+        this.dragline.attr('x1', s[0])
+              .attr('y1', s[1])
+              .attr('x2', d3.event.x)
+              .attr('y2', d3.event.y);
+      }
+    },
+
+
+    addRelationDragEnd: function() {
+      // Get the line, the endpoint service, and the target <rect>.
+      var self = this;
+      var topo = self.get('component');
+      var rect = self.get('potential_drop_point_rect');
+      var endpoint = self.get('potential_drop_point_service');
+
+      topo.buildingRelation = false;
+      self.cursorBox = null;
+
+      // If we landed on a rect, add relation, otherwise, cancel.
+      if (rect) {
+        self.ambiguousAddRelationCheck(endpoint, self, rect);
+      } else {
+        // TODO clean up, abstract
+        self.cancelRelationBuild();
+        self.addRelation(); // Will clear the state.
+      }
+    },
+    removeRelation: function(d, context, view, confirmButton) {
+      var env = this.get('component').get('env');
+      var endpoints = d.endpoints;
+      var relationElement = Y.one(context.parentNode).one('.relation');
+      utils.addSVGClass(relationElement, 'to-remove pending-relation');
+      env.remove_relation(
+          endpoints[0][0] + ':' + endpoints[0][1].name,
+          endpoints[1][0] + ':' + endpoints[1][1].name,
+          Y.bind(this._removeRelationCallback, this, view,
+          relationElement, d.relation_id, confirmButton));
+    },
+
+    _removeRelationCallback: function(view,
+            relationElement, relationId, confirmButton, ev) {
+      var db = this.get('component').get('db');
+      var service = this.get('model');
+      if (ev.err) {
+        db.notifications.add(
+            new models.Notification({
+              title: 'Error deleting relation',
+              message: 'Relation ' + ev.endpoint_a + ' to ' + ev.endpoint_b,
+              level: 'error'
+            })
+        );
+        utils.removeSVGClass(this.relationElement,
+            'to-remove pending-relation');
+      } else {
+        // Remove the relation from the DB.
+        db.relations.remove(db.relations.getById(relationId));
+        // Redraw the graph and reattach events.
+        db.fire('update');
+      }
+      view.get('rmrelation_dialog').hide();
+      view.get('rmrelation_dialog').destroy();
+      confirmButton.set('disabled', false);
+    },
+
+    removeRelationConfirm: function(d, context, view) {
+      // Destroy the dialog if it already exists to prevent cluttering
+      // up the DOM.
+      if (!Y.Lang.isUndefined(view.get('rmrelation_dialog'))) {
+        view.get('rmrelation_dialog').destroy();
+      }
+      view.set('rmrelation_dialog', views.createModalPanel(
+          'Are you sure you want to remove this relation? ' +
+              'This cannot be undone.',
+          '#rmrelation-modal-panel',
+          'Remove Relation',
+          Y.bind(function(ev) {
+            ev.preventDefault();
+            var confirmButton = ev.target;
+            confirmButton.set('disabled', true);
+            view.removeRelation(d, context, view, confirmButton);
+          },
+          this)));
+    },
+
+    cancelRelationBuild: function() {
+      var topo = this.get('component');
+      var vis = topo.vis;
+      if (this.dragline) {
+        // Get rid of our drag line
+        this.dragline.remove();
+        this.dragline = null;
+      }
+      this.clickAddRelation = null;
+      this.set('currentServiceClickAction', 'toggleControlPanel');
+      topo.buildingRelation = false;
+      topo.fire('show', { selection: vis.selectAll('.service') });
+      vis.selectAll('.service').classed('selectable-service', false);
+    },
+
+    /**
+     * An "add relation" action has been initiated by the user.
+     *
+     * @method startRelation
+     * @param {object} service The service that is the source of the
+     *  relation.
+     * @return {undefined} Side effects only.
+     */
+    startRelation: function(service) {
+      // Set flags on the view that indicate we are building a relation.
+      var topo = this.get('component');
+      var vis = topo.vis;
+
+      topo.buildingRelation = true;
+      this.clickAddRelation = true;
+
+      topo.fire('show', { selection: vis.selectAll('.service') });
+
+      var db = this.get('component').get('db');
+      var getServiceEndpoints = this.get('component')
+                                    .get('getServiceEndpoints');
+      var endpoints = models.getEndpoints(
+          service, getServiceEndpoints(), db);
+      // Transform endpoints into a list of relatable services (to the
+      // service).
+      var possible_relations = Y.Array.map(
+          Y.Array.flatten(Y.Object.values(endpoints)),
+          function(ep) {return ep.service;});
+      var invalidRelationTargets = {};
+
+      // Iterate services and invert the possibles list.
+      db.services.each(function(s) {
+        if (Y.Array.indexOf(possible_relations,
+            s.get('id')) === -1) {
+          invalidRelationTargets[s.get('id')] = true;
+        }
+      });
+
+      // Fade elements to which we can't relate.
+      // Rather than two loops this marks
+      // all services as selectable and then
+      // removes the invalid ones.
+      var sel = vis.selectAll('.service')
+              .classed('selectable-service', true)
+              .filter(function(d) {
+                return (d.id in invalidRelationTargets &&
+                          d.id !== service.id);
+              });
+      topo.fire('fade', { selection: sel });
+      sel.classed('selectable-service', false);
+
+      // Store possible endpoints.
+      this.set('addRelationStart_possibleEndpoints', endpoints);
+      // Set click action.
+      this.set('currentServiceClickAction', 'ambiguousAddRelationCheck');
+    },
+
+    /*
+         * Fired when clicking the first service in the add relation
+         * flow.
+         */
+    addRelationStart: function(m, view, context) {
+      var topo = view.get('component');
+      var service = topo.serviceForBox(m);
+      view.startRelation(service);
+      // Store start service in attrs.
+      view.set('addRelationStart_service', m);
+    },
+
+    /*
+         * Test if the pending relation is ambiguous.  Display a menu if so,
+         * create the relation if not.
+         */
+    ambiguousAddRelationCheck: function(m, view, context) {
+      var endpoints = view.get(
+          'addRelationStart_possibleEndpoints')[m.id];
+      var container = view.get('container');
+      var topo = view.get('component');
+
+      if (endpoints && endpoints.length === 1) {
+        // Create a relation with the only available endpoint.
+        var ep = endpoints[0],
+                endpoints_item = [
+                  [ep[0].service, {
+                    name: ep[0].name,
+                    role: 'server' }],
+                  [ep[1].service, {
+                    name: ep[1].name,
+                    role: 'client' }]];
+        view.addRelationEnd(endpoints_item, view, context);
+        return;
+      }
+
+      // Sort the endpoints alphabetically by relation name.
+      endpoints = endpoints.sort(function(a, b) {
+        return a[0].name + a[1].name < b[0].name + b[1].name;
+      });
+
+      // Stop rubberbanding on mousemove.
+      view.clickAddRelation = null;
+
+      // Display menu with available endpoints.
+      var menu = container.one('#ambiguous-relation-menu');
+      if (menu.one('.menu')) {
+        menu.one('.menu').remove(true);
+      }
+
+      menu.append(Templates
+              .ambiguousRelationList({endpoints: endpoints}));
+
+      // For each endpoint choice, bind an an event to 'click' to
+      // add the specified relation.
+      menu.all('li').on('click', function(evt) {
+        if (evt.currentTarget.hasClass('cancel')) {
+          return;
+        }
+        var el = evt.currentTarget,
+                endpoints_item = [
+                  [el.getData('startservice'), {
+                    name: el.getData('startname'),
+                    role: 'server' }],
+                  [el.getData('endservice'), {
+                    name: el.getData('endname'),
+                    role: 'client' }]];
+        menu.removeClass('active');
+        view.addRelationEnd(endpoints_item, view, context);
+      });
+
+      // Add a cancel item.
+      menu.one('.cancel').on('click', function(evt) {
+        menu.removeClass('active');
+        view.cancelRelationBuild();
+      });
+
+      // Display the menu at the service endpoint.
+      var tr = topo.zoom.translate();
+      var z = topo.zoom.scale();
+      menu.setStyle('top', m.y * z + tr[1]);
+      menu.setStyle('left', m.x * z + m.w * z + tr[0]);
+      menu.addClass('active');
+      topo.set('active_service', m);
+      topo.set('active_context', context);
+
+      // Firing resized will ensure the menu's positioned properly.
+      topo.fire('resized');
+    },
+
+    /*
+     * Fired when clicking the second service is clicked in the
+     * add relation flow.
+     *
+     * :param endpoints: array of two endpoints, each in the form
+     *   ['service name', {
+     *     name: 'endpoint type',
+     *     role: 'client or server'
+     *   }]
+     */
+    addRelationEnd: function(endpoints, view, context) {
+      // Redisplay all services
+      view.cancelRelationBuild();
+
+      // Get the vis, and links, build the new relation.
+      var vis = view.get('component').vis;
+      var env = view.get('component').get('env');
+      var db = view.get('component').get('db');
+      var source = view.get('addRelationStart_service');
+      var relation_id = 'pending-' + endpoints[0][0] + endpoints[1][0];
+
+      if (endpoints[0][0] === endpoints[1][0]) {
+        view.set('currentServiceClickAction', 'toggleControlPanel');
+        return;
+      }
+
+      // Create a pending relation in the database between the
+      // two services.
+      db.relations.create({
+        relation_id: relation_id,
+        display_name: 'pending',
+        endpoints: endpoints,
+        pending: true
+      });
+
+      // Firing the update event on the db will properly redraw the
+      // graph and reattach events.
+      //db.fire('update');
+      view.get('component').bindAllD3Events();
+      view.update();
+
+      // Fire event to add relation in juju.
+      // This needs to specify interface in the future.
+      env.add_relation(
+          endpoints[0][0] + ':' + endpoints[0][1].name,
+          endpoints[1][0] + ':' + endpoints[1][1].name,
+          Y.bind(this._addRelationCallback, this, view, relation_id)
+      );
+      view.set('currentServiceClickAction', 'toggleControlPanel');
+    },
+
+    _addRelationCallback: function(view, relation_id, ev) {
+      console.log('addRelationCallback reached');
+      var topo = view.get('component');
+      var db = topo.get('db');
+      var vis = topo.vis;
+      // Remove our pending relation from the DB, error or no.
+      db.relations.remove(
+          db.relations.getById(relation_id));
+      vis.select('#' + relation_id).remove();
+      if (ev.err) {
+        db.notifications.add(
+            new models.Notification({
+              title: 'Error adding relation',
+              message: 'Relation ' + ev.endpoint_a +
+                  ' to ' + ev.endpoint_b,
+              level: 'error'
+            })
+        );
+      } else {
+        // Create a relation in the database between the two services.
+        var result = ev.result;
+        var endpoints = Y.Array.map(result.endpoints, function(item) {
+          var id = Y.Object.keys(item)[0];
+          return [id, item[id]];
+        });
+        db.relations.create({
+          relation_id: ev.result.id,
+          type: result['interface'],
+          endpoints: endpoints,
+          pending: false,
+          scope: result.scope,
+          // endpoints[1][1].name should be the same
+          display_name: endpoints[0][1].name
+        });
+      }
+      // Redraw the graph and reattach events.
+      //db.fire('update');
+      view.get('component').bindAllD3Events();
+      view.update();
+    },
+
+    /*
+         * Utility function to get subordinate relations for a service.
+         */
+    subordinateRelationsForService: function(service) {
+      return this.relPairs.filter(function(p) {
+        return p.modelIds().indexOf(service.modelId()) !== -1 &&
+            p.scope === 'container';
+      });
+    },
+
+    subRelBlockMouseEnter: function(d, self) {
+      // Add an 'active' class to all of the subordinate relations
+      // belonging to this service.
+      self.subordinateRelationsForService(d)
+    .forEach(function(p) {
+            utils.addSVGClass('#' + p.id, 'active');
+          });
+    },
+
+    subRelBlockMouseLeave: function(d, self) {
+      // Remove 'active' class from all subordinate relations.
+      if (!self.keepSubRelationsVisible) {
+        utils.removeSVGClass('.subordinate-rel-group', 'active');
+      }
+    },
+
+    /**
+     * Toggle the visibility of subordinate relations for visibility
+     * or removal.
+     * @param {object} d The data-bound object (the subordinate).
+     * @param {object} self The view.
+     **/
+    subRelBlockClick: function(d, self) {
+      if (self.keepSubRelationsVisible) {
+        self.hideSubordinateRelations();
+      } else {
+        self.showSubordinateRelations(this);
+      }
+    },
+
+    /**
+     * Show subordinate relations for a service.
+     *
+     * @method showSubordinateRelations
+     * @param {Object} subordinate The sub-rel-block g element in the form
+     * of a DOM node.
+     * @return {undefined} nothing.
+     */
+    showSubordinateRelations: function(subordinate) {
+      this.keepSubRelationsVisible = true;
+      utils.addSVGClass(Y.one(subordinate).one('.sub-rel-count'), 'active');
+    },
+
+    /**
+     * Hide subordinate relations.
+     *
+     * @method hideSubordinateRelations
+     * @return {undefined} nothing.
+     */
+    hideSubordinateRelations: function() {
+      var container = this.get('container');
+      utils.removeSVGClass('.subordinate-rel-group', 'active');
+      this.keepSubRelationsVisible = false;
+      utils.removeSVGClass(container.one('.sub-rel-count.active'),
+          'active');
+    },
+
+    relationClick: function(d, self) {
+      if (d.scope === 'container') {
+        var subRelDialog = views.createModalPanel(
+            'You may not remove a subordinate relation.',
+            '#rmsubrelation-modal-panel');
+        subRelDialog.addButton(
+            { value: 'Cancel',
+              section: Y.WidgetStdMod.FOOTER,
+              /**
+               * @method action Hides the dialog on click.
+               * @param {object} e The click event.
+               * @return {undefined} nothing.
+               */
+              action: function(e) {
+                e.preventDefault();
+                subRelDialog.hide();
+                subRelDialog.destroy();
+              },
+              classNames: ['btn']
+            });
+        subRelDialog.get('boundingBox').all('.yui3-button')
+                .removeClass('yui3-button');
+      } else {
+        self.removeRelationConfirm(d, this, self);
+      }
     }
 
   }, {

=== modified file 'app/views/topology/topology.js'
--- app/views/topology/topology.js	2012-12-20 16:23:32 +0000
+++ app/views/topology/topology.js	2012-12-21 19:34:22 +0000
@@ -129,6 +129,15 @@
         .range([height, 0]);
       this.zoom.x(this.xScale)
         .y(this.yScale);
+    },
+
+    /*
+         * Utility method to get a service object from the DB
+         * given a BoundingBox.
+         */
+    serviceForBox: function(boundingBox) {
+      var db = this.get('db');
+      return db.services.getById(boundingBox.id);
     }
 
   }, {

=== modified file 'test/test_application_notifications.js'
--- test/test_application_notifications.js	2012-12-13 11:44:57 +0000
+++ test/test_application_notifications.js	2012-12-21 19:34:22 +0000
@@ -192,12 +192,12 @@
       function() {
         var view = new views.environment({db: db, container: viewContainer});
         view.render();
-        var module = view.topo.modules.MegaModule;
+        var module = view.topo.modules.RelationModule;
         // The callback wants to remove the pending relation from the db.
         db.relations.remove = NO_OP;
         // The _addRelationCallback args are: view, relation id, event.
         var args = [module, 'relation_id', ERR_EV];
-        module.service_click_actions._addRelationCallback.apply(module, args);
+        module._addRelationCallback.apply(module, args);
         assert.equal(1, db.notifications.size());
       });
 
@@ -205,7 +205,7 @@
       function() {
         var view = new views.environment({db: db, container: viewContainer});
         view.render();
-        var module = view.topo.modules.MegaModule;
+        var module = view.topo.modules.RelationModule;
         // The _removeRelationCallback args are: view, relation element,
         // relation id, confirm button, event.
         var args = [

=== modified file 'test/test_environment_view.js'
--- test/test_environment_view.js	2012-12-20 16:23:32 +0000
+++ test/test_environment_view.js	2012-12-21 19:34:22 +0000
@@ -393,10 +393,11 @@
          };
 
          // Toggle the control panel for the Add Relation button.
-         var module = view.topo.modules.MegaModule;
-         module.service_click_actions.toggleControlPanel(
+         var module = view.topo.modules.RelationModule;
+         var mega = view.topo.modules.MegaModule;
+         mega.service_click_actions.toggleControlPanel(
              d3.select(service.getDOMNode()).datum(),
-             module,
+             mega,
              service);
          // Mock an event object so that d3.mouse does not throw a NPE.
          d3.event = {};
@@ -408,7 +409,7 @@
                .size()
                .should.equal(1);
          // Start the process of adding a relation.
-         module.service_click_actions.ambiguousAddRelationCheck(
+         module.ambiguousAddRelationCheck(
              d3.select(service.next().getDOMNode()).datum(),
              module,
              service.next());
@@ -443,7 +444,7 @@
          container.all('.to-remove')
               .size()
               .should.equal(1);
-         view.topo.modules.MegaModule.get('rmrelation_dialog').hide();
+         view.topo.modules.RelationModule.get('rmrelation_dialog').hide();
        });
 
     it('must not allow removing a subordinate relation between services',
@@ -484,12 +485,14 @@
           view.render();
 
           // If the user has clicked on the "Add Relation" menu item...
-          var module = view.topo.modules.MegaModule;
+          var module = view.topo.modules.RelationModule;
+          var mega = view.topo.modules.MegaModule;
+          var topo = module.get('component');
           module.startRelation(service);
-          assert.isTrue(module.buildingRelation);
+          assert.isTrue(topo.buildingRelation);
           // ...clicking on the background causes the relation drag to stop.
-          module.backgroundClicked();
-          assert.isFalse(module.buildingRelation);
+          mega.backgroundClicked();
+          assert.isFalse(topo.buildingRelation);
         });
 
     // TODO: This will be fully testable once we have specification on the

=== modified file 'undocumented'
--- undocumented	2012-12-19 13:45:10 +0000
+++ undocumented	2012-12-21 19:34:22 +0000
@@ -1,5 +1,19 @@
 app/app.js:95 "callback"
 app/app.js:552 "callback"
+app/store/charm.js:66 "_normalizeCharms"
+app/store/charm.js:24 "find"
+app/store/charm.js:11 "success"
+app/store/charm.js:105 "setter"
+app/store/charm.js:7 "loadByPath"
+app/store/notifications.js:100 "level"
+app/store/notifications.js:148 "generate_notices"
+app/store/notifications.js:118 "evict"
+app/store/notifications.js:140 "level"
+app/store/notifications.js:108 "message"
+app/store/notifications.js:22 "title"
+app/store/notifications.js:25 "message"
+app/store/notifications.js:129 "title"
+app/store/notifications.js:137 "message"
 app/store/env.js:64 "on_close"
 app/store/env.js:69 "on_message"
 app/store/env.js:181 "status"
@@ -26,53 +40,15 @@
 app/store/env.js:86 "dispatch_result"
 app/store/env.js:139 "add_relation"
 app/store/env.js:22 "initializer"
-app/store/charm.js:66 "_normalizeCharms"
-app/store/charm.js:24 "find"
-app/store/charm.js:11 "success"
-app/store/charm.js:105 "setter"
-app/store/charm.js:7 "loadByPath"
-app/store/notifications.js:100 "level"
-app/store/notifications.js:148 "generate_notices"
-app/store/notifications.js:118 "evict"
-app/store/notifications.js:140 "level"
-app/store/notifications.js:108 "message"
-app/store/notifications.js:22 "title"
-app/store/notifications.js:25 "message"
-app/store/notifications.js:129 "title"
-app/store/notifications.js:137 "message"
-app/views/utils.js:415 "invokeCallback"
-app/views/utils.js:367 "_addAlertMessage"
-app/views/utils.js:134 "console"
-app/views/utils.js:128 "native"
-app/views/utils.js:699 "scale"
-app/views/utils.js:224 "humanizeNumber"
-app/views/utils.js:638 "get"
-app/views/utils.js:131 "noop"
-app/views/utils.js:335 "action"
-app/views/utils.js:113 "noop"
-app/views/utils.js:556 "toString"
-app/views/utils.js:612 "BoundingBox"
-app/views/utils.js:641 "set"
-app/views/utils.js:191 "bindModelView"
-app/views/utils.js:563 "isInt"
-app/views/utils.js:822 "BoxPair"
-app/views/utils.js:825 "pair"
-app/views/utils.js:247 "hasSVGClass"
-app/views/utils.js:162 "substitute"
-app/views/utils.js:255 "addSVGClass"
-app/views/utils.js:656 "set"
-app/views/utils.js:292 "toggleSVGClass"
-app/views/utils.js:648 "set"
-app/views/utils.js:655 "get"
-app/views/utils.js:700 "translate"
-app/views/utils.js:567 "isFloat"
-app/views/utils.js:214 "renderable_charm"
-app/views/utils.js:614 "Box"
-app/views/utils.js:647 "get"
-app/views/utils.js:276 "removeSVGClass"
-app/views/environment.js:24 "initializer"
-app/views/environment.js:63 "postRender"
-app/views/environment.js:31 "render"
+app/views/charm.js:32 "render"
+app/views/charm.js:96 "_deployCallback"
+app/views/charm.js:61 "on_charm_data"
+app/views/charm.js:141 "on_search_change"
+app/views/charm.js:69 "on_charm_deploy"
+app/views/charm.js:114 "initializer"
+app/views/charm.js:16 "initializer"
+app/views/charm.js:166 "on_results_change"
+app/views/charm.js:122 "render"
 app/views/charm-panel.js:1168 "calculatePanelPosition"
 app/views/charm-panel.js:476 "initializer"
 app/views/charm-panel.js:250 "render"
@@ -95,15 +71,6 @@
 app/views/charm-panel.js:964 "setupOverlay"
 app/views/charm-panel.js:192 "mouseenter"
 app/views/charm-panel.js:1207 "getInstance"
-app/views/charm.js:32 "render"
-app/views/charm.js:96 "_deployCallback"
-app/views/charm.js:61 "on_charm_data"
-app/views/charm.js:141 "on_search_change"
-app/views/charm.js:69 "on_charm_deploy"
-app/views/charm.js:114 "initializer"
-app/views/charm.js:16 "initializer"
-app/views/charm.js:166 "on_results_change"
-app/views/charm.js:122 "render"
 app/views/notifications.js:239 "render"
 app/views/notifications.js:63 "notifyToggle"
 app/views/notifications.js:93 "notificationSelect"
@@ -125,6 +92,36 @@
 app/views/unit.js:297 "retryRelation"
 app/views/unit.js:118 "confirmResolved"
 app/views/unit.js:23 "initializer"
+app/views/utils.js:415 "invokeCallback"
+app/views/utils.js:367 "_addAlertMessage"
+app/views/utils.js:134 "console"
+app/views/utils.js:128 "native"
+app/views/utils.js:699 "scale"
+app/views/utils.js:224 "humanizeNumber"
+app/views/utils.js:638 "get"
+app/views/utils.js:131 "noop"
+app/views/utils.js:335 "action"
+app/views/utils.js:113 "noop"
+app/views/utils.js:556 "toString"
+app/views/utils.js:612 "BoundingBox"
+app/views/utils.js:641 "set"
+app/views/utils.js:191 "bindModelView"
+app/views/utils.js:563 "isInt"
+app/views/utils.js:822 "BoxPair"
+app/views/utils.js:825 "pair"
+app/views/utils.js:247 "hasSVGClass"
+app/views/utils.js:162 "substitute"
+app/views/utils.js:255 "addSVGClass"
+app/views/utils.js:656 "set"
+app/views/utils.js:292 "toggleSVGClass"
+app/views/utils.js:648 "set"
+app/views/utils.js:655 "get"
+app/views/utils.js:700 "translate"
+app/views/utils.js:567 "isFloat"
+app/views/utils.js:214 "renderable_charm"
+app/views/utils.js:614 "Box"
+app/views/utils.js:647 "get"
+app/views/utils.js:276 "removeSVGClass"
 app/views/service.js:488 "updateConstraints"
 app/views/service.js:514 "_setConstraintsCallback"
 app/views/service.js:846 "filterUnits"
@@ -154,50 +151,38 @@
 app/views/service.js:23 "resetUnits"
 app/views/service.js:230 "unexposeService"
 app/views/service.js:237 "_unexposeServiceCallback"
-app/views/topology/mega.js:810 "fade"
-app/views/topology/mega.js:173 "serviceClick"
-app/views/topology/mega.js:935 "_removeRelationCallback"
-app/views/topology/mega.js:1067 "showGraphListPicker"
-app/views/topology/mega.js:1523 "_addRelationCallback"
-app/views/topology/mega.js:419 "updateLinks"
-app/views/topology/mega.js:861 "addRelationDragStart"
-app/views/topology/mega.js:850 "addRelation"
-app/views/topology/mega.js:828 "renderedHandler"
-app/views/topology/mega.js:1077 "hideGraphListPicker"
-app/views/topology/mega.js:248 "updateData"
-app/views/topology/mega.js:789 "serviceForBox"
-app/views/topology/mega.js:1325 "destroyServiceConfirm"
-app/views/topology/mega.js:1260 "subRelBlockMouseEnter"
-app/views/topology/mega.js:505 "drawRelation"
-app/views/topology/mega.js:1357 "_destroyCallback"
-app/views/topology/mega.js:754 "processRelations"
-app/views/topology/mega.js:1297 "toggleControlPanel"
-app/views/topology/mega.js:1482 "addRelationEnd"
-app/views/topology/mega.js:743 "processRelation"
-app/views/topology/mega.js:1316 "show_service"
-app/views/topology/mega.js:191 "serviceDblClick"
-app/views/topology/mega.js:798 "show"
-app/views/topology/mega.js:439 "drawRelationGroup"
-app/views/topology/mega.js:904 "addRelationDragEnd"
-app/views/topology/mega.js:886 "addRelationDrag"
-app/views/topology/mega.js:289 "update"
-app/views/topology/mega.js:1269 "subRelBlockMouseLeave"
-app/views/topology/mega.js:1196 "serviceMouseEnter"
-app/views/topology/mega.js:960 "removeRelationConfirm"
-app/views/topology/mega.js:1387 "addRelationStart"
-app/views/topology/mega.js:1398 "ambiguousAddRelationCheck"
-app/views/topology/mega.js:923 "removeRelation"
-app/views/topology/mega.js:980 "cancelRelationBuild"
-app/views/topology/mega.js:92 "callback"
-app/views/topology/mega.js:521 "drawService"
-app/views/topology/mega.js:804 "hide"
-app/views/topology/mega.js:163 "initializer"
-app/views/topology/mega.js:197 "relationClick"
-app/views/topology/mega.js:1348 "destroyService"
-app/views/topology/mega.js:1161 "updateServiceMenuLocation"
-app/views/topology/mega.js:1114 "setSizesFromViewport"
-app/views/topology/mega.js:1236 "serviceMouseLeave"
-app/views/topology/mega.js:779 "subordinateRelationsForService"
+app/views/environment.js:24 "initializer"
+app/views/environment.js:31 "render"
+app/views/environment.js:64 "postRender"
+app/views/topology/relation.js:116 "renderedHandler"
+app/views/topology/relation.js:494 "cancelRelationBuild"
+app/views/topology/relation.js:97 "render"
+app/views/topology/relation.js:437 "removeRelation"
+app/views/topology/relation.js:758 "subRelBlockMouseEnter"
+app/views/topology/relation.js:200 "drawRelationGroup"
+app/views/topology/relation.js:92 "initializer"
+app/views/topology/relation.js:582 "ambiguousAddRelationCheck"
+app/views/topology/relation.js:372 "addRelationDragStart"
+app/views/topology/relation.js:666 "addRelationEnd"
+app/views/topology/relation.js:347 "serviceMouseLeave"
+app/views/topology/relation.js:296 "addRelation"
+app/views/topology/relation.js:263 "drawRelation"
+app/views/topology/relation.js:751 "subordinateRelationsForService"
+app/views/topology/relation.js:418 "addRelationDragEnd"
+app/views/topology/relation.js:102 "update"
+app/views/topology/relation.js:767 "subRelBlockMouseLeave"
+app/views/topology/relation.js:42 "callback"
+app/views/topology/relation.js:132 "processRelations"
+app/views/topology/relation.js:306 "serviceMouseEnter"
+app/views/topology/relation.js:707 "_addRelationCallback"
+app/views/topology/relation.js:474 "removeRelationConfirm"
+app/views/topology/relation.js:449 "_removeRelationCallback"
+app/views/topology/relation.js:396 "addRelationDrag"
+app/views/topology/relation.js:570 "addRelationStart"
+app/views/topology/relation.js:120 "processRelation"
+app/views/topology/relation.js:154 "updateLinks"
+app/views/topology/relation.js:815 "relationClick"
+app/views/topology/relation.js:278 "updateSubordinateRelationsCount"
 app/views/topology/panzoom.js:83 "update"
 app/views/topology/panzoom.js:40 "zoomHandler"
 app/views/topology/panzoom.js:100 "zoom_in"
@@ -207,26 +192,51 @@
 app/views/topology/panzoom.js:157 "renderedHandler"
 app/views/topology/panzoom.js:91 "zoom_out"
 app/views/topology/panzoom.js:48 "renderSlider"
-app/views/topology/relation.js:18 "render"
-app/views/topology/relation.js:23 "update"
-app/views/topology/relation.js:14 "initializer"
+app/views/topology/mega.js:617 "hideGraphListPicker"
+app/views/topology/mega.js:769 "destroyService"
+app/views/topology/mega.js:737 "show_service"
+app/views/topology/mega.js:607 "showGraphListPicker"
+app/views/topology/mega.js:117 "initializer"
+app/views/topology/mega.js:717 "toggleControlPanel"
+app/views/topology/mega.js:127 "serviceClick"
+app/views/topology/mega.js:546 "hide"
+app/views/topology/mega.js:571 "renderedHandler"
+app/views/topology/mega.js:778 "_destroyCallback"
+app/views/topology/mega.js:627 "setSizesFromViewport"
+app/views/topology/mega.js:145 "serviceDblClick"
+app/views/topology/mega.js:540 "show"
+app/views/topology/mega.js:552 "fade"
+app/views/topology/mega.js:675 "updateServiceMenuLocation"
+app/views/topology/mega.js:216 "update"
+app/views/topology/mega.js:176 "updateData"
+app/views/topology/mega.js:746 "destroyServiceConfirm"
+app/views/topology/mega.js:316 "drawService"
+app/views/topology/topology.js:170 "setter"
+app/views/topology/topology.js:162 "getter"
+app/views/topology/topology.js:58 "renderOnce"
+app/views/topology/topology.js:120 "sizeChangeHandler"
+app/views/topology/topology.js:26 "initializer"
+app/views/topology/topology.js:178 "getter"
+app/views/topology/topology.js:169 "getter"
+app/views/topology/topology.js:163 "setter"
+app/views/topology/topology.js:138 "serviceForBox"
+app/views/topology/topology.js:174 "getter"
+app/views/topology/viewport.js:33 "initializer"
+app/views/topology/viewport.js:66 "update"
+app/views/topology/viewport.js:37 "render"
 app/views/topology/service.js:46 "render"
 app/views/topology/service.js:33 "componentBound"
 app/views/topology/service.js:26 "initializer"
 app/views/topology/service.js:50 "update"
 app/views/topology/service.js:39 "_scaleLayout"
-app/views/topology/topology.js:161 "setter"
-app/views/topology/topology.js:26 "initializer"
-app/views/topology/topology.js:154 "setter"
-app/views/topology/topology.js:165 "getter"
-app/views/topology/topology.js:120 "sizeChangeHandler"
-app/views/topology/topology.js:160 "getter"
-app/views/topology/topology.js:153 "getter"
-app/views/topology/topology.js:169 "getter"
-app/views/topology/topology.js:58 "renderOnce"
-app/views/topology/viewport.js:33 "initializer"
-app/views/topology/viewport.js:66 "update"
-app/views/topology/viewport.js:37 "render"
+app/models/endpoints.js:43 "add"
+app/models/endpoints.js:32 "convert"
+app/models/charm.js:155 "validator"
+app/models/charm.js:113 "parse"
+app/models/charm.js:77 "sync"
+app/models/charm.js:105 "failure"
+app/models/charm.js:48 "initializer"
+app/models/charm.js:129 "compare"
 app/models/models.js:305 "setter"
 app/models/models.js:430 "getModelListByModelName"
 app/models/models.js:325 "add"
@@ -251,11 +261,3 @@
 app/models/models.js:345 "removeOldest"
 app/models/models.js:240 "has_relation_for_endpoint"
 app/models/models.js:231 "process_delta"
-app/models/endpoints.js:43 "add"
-app/models/endpoints.js:32 "convert"
-app/models/charm.js:155 "validator"
-app/models/charm.js:113 "parse"
-app/models/charm.js:77 "sync"
-app/models/charm.js:105 "failure"
-app/models/charm.js:48 "initializer"
-app/models/charm.js:129 "compare"


Follow ups