yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #02110
[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