API Docs for:
Show:

File: app/views/topology/relation.js

'use strict';

/**
 * Provide the RelationModule class.
 *
 * @module topology
 * @submodule topology.relation
 */

YUI.add('juju-topology-relation', function(Y) {
  var views = Y.namespace('juju.views'),
      models = Y.namespace('juju.models'),
      utils = Y.namespace('juju.views.utils'),
      d3ns = Y.namespace('d3'),
      Templates = views.Templates;

  /**
   * Manage relation rendering and events.
   *
   * ## Emitted events:
   *
   * - *clearState:* clear all possible states that the environment view can
   *   be in as it pertains to actions (building a relation, viewing a service
   *   menu, etc.)
   * - *hideServiceMenu:* hide the service menu after the 'Add Relation' item
   *   was clicked.
   * - *fade:* fade services that are not valid endpoints for a pending
   *   relation.
   * - *show:* show faded services at 100% opacity again.
   * - *resized:* ensure that menus are positioned properly.
   *
   * @class RelationModule
   */
  var RelationModule = Y.Base.create('RelationModule', d3ns.Module, [], {

    events: {
      scene: {
        '.sub-rel-block': {
          mouseenter: 'subRelBlockMouseEnter',
          mouseleave: 'subRelBlockMouseLeave',
          click: 'subRelBlockClick'
        },
        '.rel-label': {
          click: 'relationClick',
          mousemove: 'mousemove'
        },
        '.dragline': {
          /**
           * The user clicked while the dragline was active.
           *
           * @method events.scene.dragline.click
           */
          click: {callback: 'draglineClicked'}
        },
        '.add-relation': {
          /**
           * The user clicked on the "Build Relation" menu item.
           *
           * @method events.scene.add-relation.click
           */
          click: {callback: 'addRelButtonClicked'}
        },
        '.zoom-plane': {
          mousemove: {callback: 'mousemove'}
        }
      },
      yui: {
        /**
          Ensure the dragline follows the cursor when moved.

          @event addRelationDrag
        */
        addRelationDrag: 'addRelationDrag',
        /**
          Complete adding a relation when dragging to create it.

          @event addRelationDragEnd
          @param {Object} box The starting service's BoxModel.
        */
        addRelationDragEnd: 'addRelationDragEnd',
        /**
          Begin the process of adding a relation when dragging to create it.

          @event addRelationDragStart
          @param {Object} box The ending service's BoxModel.
        */
        addRelationDragStart: 'addRelationDragStart',
        /**
          Cancel building a relation when dragging to create it.

          @event cancelRelationBuild
          @param {Object} box The starting service's BoxModel.
        */
        cancelRelationBuild: 'cancelRelationBuild',
        /**
          Clear view state as pertaining to relations.

          @event clearState
        */
        clearState: 'clearState',
        /**
          Ensure that mousemove events bubble to canvas when moving over a
          relation line/label.

          @event mouseMove
        */
        mouseMove: 'mouseMoveHandler',
        /**
          Render relations at an appropriate time.

          @event rendered
        */
        rendered: 'renderedHandler',
        /**
          Update the endpoints for relations when services are moved.

          @event updateLinkEndpoints
          @param {Object} service The service which has had its position
                                  updated.
        */
        serviceMoved: 'updateLinkEndpoints',
        /**
          Update relations after services are rendered.

          @event servicesRendered
          @param {Object} service The model for the service that was moved.
        */
        servicesRendered: 'updateLinks',
        /**
          Ensure the dragline follows the cursor outside of services.

          @event snapOutOfService
        */
        snapOutOfService: 'snapOutOfService',
        /**
          Ensure the dragline snaps to service when the cursor is inside one.

          @event snapToService
          @param {Object} d The service model wrapped in a BoxModel object.
          @param {Object} rect The SVG rect element for the service.
        */
        snapToService: 'snapToService'
      }
    },

    initializer: function(options) {
      RelationModule.superclass.constructor.apply(this, arguments);
      this.relations = [];
    },

    render: function() {
      RelationModule.superclass.render.apply(this, arguments);
      return this;
    },

    update: function() {
      RelationModule.superclass.update.apply(this, arguments);

      var topo = this.get('component');
      var db = topo.get('db');
      var self = this;
      var relations = db.relations.toArray();
      this.relations = this.decorateRelations(relations);
      this.updateLinks();
      this.updateSubordinateRelationsCount();

      // Ensure that link endpoints are up-to-date.
      Y.each(topo.service_boxes, function(svc, key) {
        self.updateLinkEndpoints({ service: svc });
      });

      return this;
    },

    renderedHandler: function() {
      this.update();
    },

    processRelation: function(relation) {
      var self = this;
      var topo = self.get('component');
      var endpoints = relation.get('endpoints');
      var rel_services = [];

      Y.each(endpoints, function(endpoint) {
        rel_services.push([endpoint[1].name, topo.service_boxes[endpoint[0]]]);
      });
      return rel_services;
    },

    /**
     *
     * @method decorateRelations
     * @param {Array} relations The relations currently in effect.
     * @return {Array} Relation pairs.
     */
    decorateRelations: function(relations) {
      var self = this;
      var decorated = [];
      Y.each(relations, function(relation) {
        var pair = self.processRelation(relation);

        // skip peer for now
        if (pair.length === 2) {
          var source = pair[0][1];
          var target = pair[1][1];
          var decoratedRelation = views.DecoratedRelation(
              relation, source, target);
          // Copy the relation type to the box.
          if (decoratedRelation.display_name === undefined) {
            decoratedRelation.display_name = pair[0][0];
          }
          decorated.push(decoratedRelation);
        }
      });
      return decorated;
    },

    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} evt The event facade that was fired.  This should have
     *                     a 'service' property mixed in when fired.
     */
    updateLinkEndpoints: function(evt) {
      var self = this;
      var service = evt.service;
      Y.each(Y.Array.filter(self.relations, function(relation) {
        return relation.source.id === service.id ||
            relation.target.id === service.id;
      }), function(relation) {
        var rel_group = d3.select('#' + utils.generateSafeDOMId(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.relations,
          function(r) {
            return r.compositeId;
          });

      var enter = g.enter();

      enter.insert('g', 'g.service')
              .attr('id', function(d) {
            return utils.generateSafeDOMId(d.id);
          })
              .attr('class', function(d) {
                // Mark the rel-group as a subordinate relation if need be.
                return (d.isSubordinate ? '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.isSubordinate ? '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.subordinate')
        .selectAll('.sub-rel-block tspan')
        .text(function(d) {
            return self.subordinateRelationsForService(d).length;
          });
    },

    draglineClicked: function(d, self) {
      // It was technically the dragline that was clicked, but the
      // intent was to click on the background, so...
      var topo = self.get('component');
      topo.fire('clearState');
    },

    addRelButtonClicked: 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');
      var container = context.get('container');

      // Remove the service menu.
      topo.fire('hideServiceMenu');

      // Create the dragline and position its endpoints properly.
      context.addRelationDragStart({service: box});
      context.mousemove.call(
          container.one('.topology g').getDOMNode(),
          null, context);
      context.addRelationStart(box, context, origin);
    },

    /*
     * 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', 'hideServiceMenu');
      } // Otherwise do nothing.
    },

    /**
     * If the mouse moves and we are adding a relation, then the dragline
     * needs to be updated.
     *
     * @method mousemove
     * @param {object} d Unused.
     * @param {object} self The environment view itself.
     * @return {undefined} Side effects only.
     */
    mousemove: function(d, self) {
      if (self.clickAddRelation) {
        var mouse = d3.mouse(this);
        var box = self.get('addRelationStart_service');
        d3.event.x = mouse[0];
        d3.event.y = mouse[1];
        self.addRelationDrag.call(self, {box: box});
      }
    },

    /**
     * Handler for when the mouse is moved over a service.
     *
     * @method mouseMoveHandler
     * @param {object} evt Event facade.
     * @return {undefined} Side effects only.
     */
    mouseMoveHandler: function(evt) {
      var container = this.get('container');
      this.mousemove.call(
          container.one('.zoom-plane').getDOMNode(),
          null, this);
    },

    snapToService: function(evt) {
      var d = evt.service;
      var rect = evt.rect;

      // Do not fire if we're on the same service.
      if (d === this.get('addRelationStart_service')) {
        return;
      }
      this.set('potential_drop_point_service', d);
      this.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 (this.dragline) {
        var connectors = d.getConnectorPair(
            this.get('addRelationStart_service'));
        var s = connectors[0];
        var t = connectors[1];
        this.dragline.attr('x1', t[0])
        .attr('y1', t[1])
        .attr('x2', s[0])
        .attr('y2', s[1])
        .attr('class', 'relation pending-relation dragline');
        this.draglineOverService = true;
      }
    },

    snapOutOfService: function() {
      // Do not fire if we aren't looking for a relation endpoint.
      if (!this.get('potential_drop_point_rect')) {
        return;
      }

      this.set('potential_drop_point_service', null);
      this.set('potential_drop_point_rect', null);

      if (this.dragline) {
        this.dragline.attr('class',
            'relation pending-relation dragline dragging');
        this.draglineOverService = false;
      }
    },

    addRelationDragStart: function(evt) {
      var d = evt.service;
      // 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;
      var container = this.get('container');

      // Start the line between the cursor and the nearest connector
      // point on the service.
      var mouse = d3.mouse(container.one('svg g').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);
    },

    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(relation, view, confirmButton) {
      var env = this.get('component').get('env');
      // At this time, relations may have been redrawn, so here we have to
      // retrieve the relation DOM element again.
      var relationElement = view.get('container')
        .one('#' + utils.generateSafeDOMId(relation.relation_id));
      utils.addSVGClass(relationElement, 'to-remove pending-relation');
      env.remove_relation(relation.endpoints[0], relation.endpoints[1],
          Y.bind(this._removeRelationCallback, this, view,
          relationElement, relation.relation_id, confirmButton));
    },

    _removeRelationCallback: function(view,
            relationElement, relationId, confirmButton, ev) {
      var topo = this.get('component'),
          db = topo.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.
        topo.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, view, confirmButton);
          },
          this)));
    },

    /**
     * Clear any states such as building a relation or showing
     * subordinate relations.
     *
     * @method clearState
     * @return {undefined} side effects only.
     */
    clearState: function() {
      this.cancelRelationBuild();
      this.hideSubordinateRelations();
    },

    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', 'hideServiceMenu');
      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 = topo.get('db');
      var endpointsController = topo.get('endpointsController');
      var endpoints = models.getEndpoints(service, endpointsController);

      // 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];
        var 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, delegate a click event to add the specified
      // relation. Use event delegation in order to avoid weird behaviors
      // encountered when using "on" on a YUI NodeList: in some situations,
      // e.g. our production server, NodeList.on does not work.
      menu.one('.menu').delegate('click', function(evt) {
        var el = evt.target;
        var 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);
      }, 'li');

      // 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 the second service is clicked in the
     * add relation flow.
     *
     * @method addRelationEnd
     * @param endpoints {Array} array of two endpoints, each in the form
     *   ['service name', {
     *     name: 'endpoint type',
     *     role: 'client or server'
     *   }].
     * @param module {RelationModule}
     * @return undefined Side-effects only.
     */
    addRelationEnd: function(endpoints, module) {
      // Redisplay all services
      module.cancelRelationBuild();

      // Get the vis, and links, build the new relation.
      var topo = module.get('component');
      var vis = topo.vis;
      var env = topo.get('env');
      var db = topo.get('db');
      var source = module.get('addRelationStart_service');
      var relation_id = 'pending-' + endpoints[0][0] + endpoints[1][0];

      if (endpoints[0][0] === endpoints[1][0]) {
        module.set('currentServiceClickAction', 'hideServiceMenu');
        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.
      topo.update();
      topo.bindAllD3Events();
      // Fire event to add relation in juju.
      // This needs to specify interface in the future.
      env.add_relation(endpoints[0], endpoints[1],
          Y.bind(this._addRelationCallback, this, module, relation_id)
      );
      module.set('currentServiceClickAction', 'hideServiceMenu');
    },

    _addRelationCallback: function(module, relation_id, ev) {
      console.log('addRelationCallback reached');
      var topo = module.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('#' + utils.generateSafeDOMId(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: result.id,
          type: result['interface'],
          endpoints: endpoints,
          pending: false,
          scope: result.scope,
          // Using either the relation name for endpoint A (this one) or
          // the one for endpoint B (endpoints[1][1].name) is arbitrary.
          display_name: endpoints[0][1].name
        });
      }
      topo.update();
      topo.bindAllD3Events();
    },

    /*
     * Utility function to get subordinate relations for a service.
     */
    subordinateRelationsForService: function(service) {
      return this.relations.filter(function(relation) {
        return (relation.source.modelId === service.modelId ||
            relation.target.modelId === service.modelId) &&
            relation.isSubordinate;
      });
    },

    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('#' + utils.generateSafeDOMId(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.
     * @method subRelBlockClick
     */
    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(relation, self) {
      if (relation.isSubordinate) {
        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(relation, this, self);
      }
    }

  }, {
    ATTRS: {}

  });
  views.RelationModule = RelationModule;
}, '0.1.0', {
  requires: [
    'd3',
    'd3-components',
    'node',
    'event',
    'juju-models',
    'juju-env'
  ]
});