OLD | NEW |
1 'use strict'; | 1 'use strict'; |
2 | 2 |
3 YUI.add('juju-topology-relation', function(Y) { | 3 YUI.add('juju-topology-relation', function(Y) { |
4 var views = Y.namespace('juju.views'), | 4 var views = Y.namespace('juju.views'), |
5 models = Y.namespace('juju.models'), | 5 models = Y.namespace('juju.models'), |
6 d3ns = Y.namespace('d3'); | 6 utils = Y.namespace('juju.views.utils'), |
| 7 d3ns = Y.namespace('d3'), |
| 8 Templates = views.Templates; |
7 | 9 |
8 /** | 10 /** |
9 * @module topology-relations | 11 * @module topology-relations |
10 * @class RelationModule | 12 * @class RelationModule |
11 * @namespace views | 13 * @namespace views |
12 **/ | 14 **/ |
13 var RelationModule = Y.Base.create('RelationModule', d3ns.Module, [], { | 15 var RelationModule = Y.Base.create('RelationModule', d3ns.Module, [], { |
| 16 |
| 17 events: { |
| 18 scene: { |
| 19 '.sub-rel-block': { |
| 20 mouseenter: 'subRelBlockMouseEnter', |
| 21 mouseleave: 'subRelBlockMouseLeave', |
| 22 click: 'subRelBlockClick' |
| 23 }, |
| 24 '.rel-label': { |
| 25 click: 'relationClick' |
| 26 }, |
| 27 '.dragline': { |
| 28 /** The user clicked while the dragline was active. */ |
| 29 click: {callback: 'draglineClicked'} |
| 30 }, |
| 31 '.add-relation': { |
| 32 /** The user clicked on the "Build Relation" menu item. */ |
| 33 click: {callback: 'addRelButtonClicked'} |
| 34 } |
| 35 }, |
| 36 yui: { |
| 37 rendered: {callback: 'renderedHandler'}, |
| 38 clearState: {callback: 'cancelRelationBuild'}, |
| 39 serviceMoved: {callback: 'updateLinkEndpoints'}, |
| 40 servicesRendered: {callback: 'updateLinks'}, |
| 41 snapToService: {callback: 'snapToService'}, |
| 42 snapOutOfService: {callback: 'snapOutOfService'}, |
| 43 cancelRelationBuild: {callback: 'cancelRelationBuild'}, |
| 44 addRelationDragStart: {callback: 'addRelationDragStart'}, |
| 45 addRelationDrag: {callback: 'addRelationDrag'}, |
| 46 addRelationDragEnd: {callback: 'addRelationDragEnd'} |
| 47 } |
| 48 }, |
| 49 |
14 initializer: function(options) { | 50 initializer: function(options) { |
15 RelationModule.superclass.constructor.apply(this, arguments); | 51 RelationModule.superclass.constructor.apply(this, arguments); |
| 52 this.relPairs = []; |
16 }, | 53 }, |
17 | 54 |
18 render: function() { | 55 render: function() { |
19 RelationModule.superclass.render.apply(this, arguments); | 56 RelationModule.superclass.render.apply(this, arguments); |
20 return this; | 57 return this; |
21 }, | 58 }, |
22 | 59 |
23 update: function() { | 60 update: function() { |
24 RelationModule.superclass.update.apply(this, arguments); | 61 RelationModule.superclass.update.apply(this, arguments); |
| 62 |
| 63 var topo = this.get('component'); |
| 64 var db = topo.get('db'); |
| 65 var relations = db.relations.toArray(); |
| 66 this.relPairs = this.processRelations(relations); |
| 67 topo.relPairs = this.relPairs; |
| 68 this.updateLinks(); |
| 69 this.updateSubordinateRelationsCount(); |
| 70 |
25 return this; | 71 return this; |
| 72 }, |
| 73 |
| 74 renderedHandler: function() { |
| 75 this.update(); |
| 76 }, |
| 77 |
| 78 processRelation: function(r) { |
| 79 var self = this; |
| 80 var topo = self.get('component'); |
| 81 var endpoints = r.get('endpoints'); |
| 82 var rel_services = []; |
| 83 |
| 84 Y.each(endpoints, function(ep) { |
| 85 rel_services.push([ep[1].name, topo.service_boxes[ep[0]]]); |
| 86 }); |
| 87 return rel_services; |
| 88 }, |
| 89 |
| 90 processRelations: function(rels) { |
| 91 var self = this; |
| 92 var pairs = []; |
| 93 Y.each(rels, function(rel) { |
| 94 var pair = self.processRelation(rel); |
| 95 |
| 96 // skip peer for now |
| 97 if (pair.length === 2) { |
| 98 var bpair = views.BoxPair() |
| 99 .model(rel) |
| 100 .source(pair[0][1]) |
| 101 .target(pair[1][1]); |
| 102 // Copy the relation type to the box. |
| 103 if (bpair.display_name === undefined) { |
| 104 bpair.display_name = pair[0][0]; |
| 105 } |
| 106 pairs.push(bpair); |
| 107 } |
| 108 }); |
| 109 return pairs; |
| 110 }, |
| 111 |
| 112 updateLinks: function() { |
| 113 // Enter. |
| 114 var g = this.drawRelationGroup(); |
| 115 var link = g.selectAll('line.relation'); |
| 116 |
| 117 // Update (+ enter selection). |
| 118 link.each(this.drawRelation); |
| 119 |
| 120 // Exit |
| 121 g.exit().remove(); |
| 122 }, |
| 123 |
| 124 /** |
| 125 * Update relation line endpoints for a given service. |
| 126 * |
| 127 * @method updateLinkEndpoints |
| 128 * @param {Object} service The service module that has been moved. |
| 129 */ |
| 130 updateLinkEndpoints: function(evt) { |
| 131 var self = this; |
| 132 var service = evt.service; |
| 133 Y.each(Y.Array.filter(self.relPairs, function(relation) { |
| 134 return relation.source() === service || |
| 135 relation.target() === service; |
| 136 }), function(relation) { |
| 137 var rel_group = d3.select('#' + relation.id); |
| 138 var connectors = relation.source() |
| 139 .getConnectorPair(relation.target()); |
| 140 var s = connectors[0]; |
| 141 var t = connectors[1]; |
| 142 rel_group.select('line') |
| 143 .attr('x1', s[0]) |
| 144 .attr('y1', s[1]) |
| 145 .attr('x2', t[0]) |
| 146 .attr('y2', t[1]); |
| 147 rel_group.select('.rel-label') |
| 148 .attr('transform', function(d) { |
| 149 return 'translate(' + |
| 150 [Math.max(s[0], t[0]) - |
| 151 Math.abs((s[0] - t[0]) / 2), |
| 152 Math.max(s[1], t[1]) - |
| 153 Math.abs((s[1] - t[1]) / 2)] + ')'; |
| 154 }); |
| 155 }); |
| 156 }, |
| 157 |
| 158 drawRelationGroup: function() { |
| 159 // Add a labelgroup. |
| 160 var self = this; |
| 161 var vis = this.get('component').vis; |
| 162 var g = vis.selectAll('g.rel-group') |
| 163 .data(self.relPairs, function(r) { |
| 164 return r.modelIds(); |
| 165 }); |
| 166 |
| 167 var enter = g.enter(); |
| 168 |
| 169 enter.insert('g', 'g.service') |
| 170 .attr('id', function(d) { |
| 171 return d.id; |
| 172 }) |
| 173 .attr('class', function(d) { |
| 174 // Mark the rel-group as a subordinate relation if need be. |
| 175 return (d.scope === 'container' ? |
| 176 'subordinate-rel-group ' : '') + |
| 177 'rel-group'; |
| 178 }) |
| 179 .append('svg:line', 'g.service') |
| 180 .attr('class', function(d) { |
| 181 // Style relation lines differently depending on status. |
| 182 return (d.pending ? 'pending-relation ' : '') + |
| 183 (d.scope === 'container' ? 'subordinate-relation ' : '') + |
| 184 'relation'; |
| 185 }); |
| 186 |
| 187 g.selectAll('.rel-label').remove(); |
| 188 g.selectAll('text').remove(); |
| 189 g.selectAll('rect').remove(); |
| 190 var label = g.append('g') |
| 191 .attr('class', 'rel-label') |
| 192 .attr('transform', function(d) { |
| 193 // XXX: This has to happen on update, not enter |
| 194 var connectors = d.source().getConnectorPair(d.target()); |
| 195 var s = connectors[0]; |
| 196 var t = connectors[1]; |
| 197 return 'translate(' + |
| 198 [Math.max(s[0], t[0]) - |
| 199 Math.abs((s[0] - t[0]) / 2), |
| 200 Math.max(s[1], t[1]) - |
| 201 Math.abs((s[1] - t[1]) / 2)] + ')'; |
| 202 }); |
| 203 label.append('text') |
| 204 .append('tspan') |
| 205 .text(function(d) {return d.display_name; }); |
| 206 label.insert('rect', 'text') |
| 207 .attr('width', function(d) { |
| 208 return d.display_name.length * 10 + 10; |
| 209 }) |
| 210 .attr('height', 20) |
| 211 .attr('x', function() { |
| 212 return -parseInt(d3.select(this).attr('width'), 10) / 2; |
| 213 }) |
| 214 .attr('y', -10) |
| 215 .attr('rx', 10) |
| 216 .attr('ry', 10); |
| 217 |
| 218 return g; |
| 219 }, |
| 220 |
| 221 drawRelation: function(relation) { |
| 222 var connectors = relation.source() |
| 223 .getConnectorPair(relation.target()); |
| 224 var s = connectors[0]; |
| 225 var t = connectors[1]; |
| 226 var link = d3.select(this); |
| 227 |
| 228 link |
| 229 .attr('x1', s[0]) |
| 230 .attr('y1', s[1]) |
| 231 .attr('x2', t[0]) |
| 232 .attr('y2', t[1]); |
| 233 return link; |
| 234 }, |
| 235 |
| 236 updateSubordinateRelationsCount: function() { |
| 237 var topo = this.get('component'); |
| 238 var vis = topo.vis; |
| 239 var self = this; |
| 240 |
| 241 vis.selectAll('.service') |
| 242 .filter(function(d) { |
| 243 return d.subordinate; |
| 244 }) |
| 245 .select('.sub-rel-block tspan') |
| 246 .text(function(d) { |
| 247 return self.subordinateRelationsForService(d).length; |
| 248 }); |
| 249 }, |
| 250 |
| 251 draglineClicked: function(d, self) { |
| 252 // It was technically the dragline that was clicked, but the |
| 253 // intent was to click on the background, so... |
| 254 self.backgroundClicked(); |
| 255 }, |
| 256 |
| 257 addRelButtonClicked: function(data, context) { |
| 258 var topo = context.get('component'); |
| 259 var box = topo.get('active_service'); |
| 260 var service = topo.serviceForBox(box); |
| 261 var origin = topo.get('active_context'); |
| 262 context.addRelationDragStart({service: box}); |
| 263 topo.fire('toggleControlPanel'); |
| 264 context.addRelationStart(box, context, origin); |
| 265 }, |
| 266 |
| 267 /* |
| 268 * Event handler for the add relation button. |
| 269 */ |
| 270 addRelation: function(evt) { |
| 271 var curr_action = this.get('currentServiceClickAction'); |
| 272 if (curr_action === 'show_service') { |
| 273 this.set('currentServiceClickAction', 'addRelationStart'); |
| 274 } else if (curr_action === 'addRelationStart' || |
| 275 curr_action === 'ambiguousAddRelationCheck') { |
| 276 this.set('currentServiceClickAction', 'toggleControlPanel'); |
| 277 } // Otherwise do nothing. |
| 278 }, |
| 279 |
| 280 snapToService: function(evt) { |
| 281 var d = evt.service; |
| 282 var rect = evt.rect; |
| 283 |
| 284 // Do not fire if we're on the same service. |
| 285 if (d === this.get('addRelationStart_service')) { |
| 286 return; |
| 287 } |
| 288 this.set('potential_drop_point_service', d); |
| 289 this.set('potential_drop_point_rect', rect); |
| 290 utils.addSVGClass(rect, 'hover'); |
| 291 |
| 292 // If we have an active dragline, stop redrawing it on mousemove |
| 293 // and draw the line between the two nearest connector points of |
| 294 // the two services. |
| 295 if (this.dragline) { |
| 296 var connectors = d.getConnectorPair( |
| 297 this.get('addRelationStart_service')); |
| 298 var s = connectors[0]; |
| 299 var t = connectors[1]; |
| 300 this.dragline.attr('x1', t[0]) |
| 301 .attr('y1', t[1]) |
| 302 .attr('x2', s[0]) |
| 303 .attr('y2', s[1]) |
| 304 .attr('class', 'relation pending-relation dragline'); |
| 305 this.draglineOverService = true; |
| 306 } |
| 307 }, |
| 308 |
| 309 snapOutOfService: function() { |
| 310 // Do not fire if we aren't looking for a relation endpoint. |
| 311 if (!this.get('potential_drop_point_rect')) { |
| 312 return; |
| 313 } |
| 314 |
| 315 this.set('potential_drop_point_service', null); |
| 316 this.set('potential_drop_point_rect', null); |
| 317 |
| 318 if (this.dragline) { |
| 319 this.dragline.attr('class', |
| 320 'relation pending-relation dragline dragging'); |
| 321 this.draglineOverService = false; |
| 322 } |
| 323 }, |
| 324 |
| 325 addRelationDragStart: function(evt) { |
| 326 var d = evt.service; |
| 327 // Create a pending drag-line. |
| 328 var vis = this.get('component').vis; |
| 329 var dragline = vis.append('line') |
| 330 .attr('class', |
| 331 'relation pending-relation dragline dragging'); |
| 332 var self = this; |
| 333 |
| 334 // Start the line between the cursor and the nearest connector |
| 335 // point on the service. |
| 336 var mouse = d3.mouse(Y.one('.topology svg').getDOMNode()); |
| 337 self.cursorBox = new views.BoundingBox(); |
| 338 self.cursorBox.pos = {x: mouse[0], y: mouse[1], w: 0, h: 0}; |
| 339 var point = self.cursorBox.getConnectorPair(d); |
| 340 dragline.attr('x1', point[0][0]) |
| 341 .attr('y1', point[0][1]) |
| 342 .attr('x2', point[1][0]) |
| 343 .attr('y2', point[1][1]); |
| 344 self.dragline = dragline; |
| 345 |
| 346 // Start the add-relation process. |
| 347 self.addRelationStart(d, self); |
| 348 }, |
| 349 |
| 350 addRelationDrag: function(evt) { |
| 351 var d = evt.box; |
| 352 |
| 353 // Rubberband our potential relation line if we're not currently |
| 354 // hovering over a potential drop-point. |
| 355 if (!this.get('potential_drop_point_service') && |
| 356 !this.draglineOverService) { |
| 357 // Create a BoundingBox for our cursor. |
| 358 this.cursorBox.pos = {x: d3.event.x, y: d3.event.y, w: 0, h: 0}; |
| 359 |
| 360 // Draw the relation line from the connector point nearest the |
| 361 // cursor to the cursor itself. |
| 362 var connectors = this.cursorBox.getConnectorPair(d), |
| 363 s = connectors[1]; |
| 364 this.dragline.attr('x1', s[0]) |
| 365 .attr('y1', s[1]) |
| 366 .attr('x2', d3.event.x) |
| 367 .attr('y2', d3.event.y); |
| 368 } |
| 369 }, |
| 370 |
| 371 |
| 372 addRelationDragEnd: function() { |
| 373 // Get the line, the endpoint service, and the target <rect>. |
| 374 var self = this; |
| 375 var topo = self.get('component'); |
| 376 var rect = self.get('potential_drop_point_rect'); |
| 377 var endpoint = self.get('potential_drop_point_service'); |
| 378 |
| 379 topo.buildingRelation = false; |
| 380 self.cursorBox = null; |
| 381 |
| 382 // If we landed on a rect, add relation, otherwise, cancel. |
| 383 if (rect) { |
| 384 self.ambiguousAddRelationCheck(endpoint, self, rect); |
| 385 } else { |
| 386 // TODO clean up, abstract |
| 387 self.cancelRelationBuild(); |
| 388 self.addRelation(); // Will clear the state. |
| 389 } |
| 390 }, |
| 391 removeRelation: function(d, context, view, confirmButton) { |
| 392 var env = this.get('component').get('env'); |
| 393 var endpoints = d.endpoints; |
| 394 var relationElement = Y.one(context.parentNode).one('.relation'); |
| 395 utils.addSVGClass(relationElement, 'to-remove pending-relation'); |
| 396 env.remove_relation( |
| 397 endpoints[0][0] + ':' + endpoints[0][1].name, |
| 398 endpoints[1][0] + ':' + endpoints[1][1].name, |
| 399 Y.bind(this._removeRelationCallback, this, view, |
| 400 relationElement, d.relation_id, confirmButton)); |
| 401 }, |
| 402 |
| 403 _removeRelationCallback: function(view, |
| 404 relationElement, relationId, confirmButton, ev) { |
| 405 var db = this.get('component').get('db'); |
| 406 var service = this.get('model'); |
| 407 if (ev.err) { |
| 408 db.notifications.add( |
| 409 new models.Notification({ |
| 410 title: 'Error deleting relation', |
| 411 message: 'Relation ' + ev.endpoint_a + ' to ' + ev.endpoint_b, |
| 412 level: 'error' |
| 413 }) |
| 414 ); |
| 415 utils.removeSVGClass(this.relationElement, |
| 416 'to-remove pending-relation'); |
| 417 } else { |
| 418 // Remove the relation from the DB. |
| 419 db.relations.remove(db.relations.getById(relationId)); |
| 420 // Redraw the graph and reattach events. |
| 421 db.fire('update'); |
| 422 } |
| 423 view.get('rmrelation_dialog').hide(); |
| 424 view.get('rmrelation_dialog').destroy(); |
| 425 confirmButton.set('disabled', false); |
| 426 }, |
| 427 |
| 428 removeRelationConfirm: function(d, context, view) { |
| 429 // Destroy the dialog if it already exists to prevent cluttering |
| 430 // up the DOM. |
| 431 if (!Y.Lang.isUndefined(view.get('rmrelation_dialog'))) { |
| 432 view.get('rmrelation_dialog').destroy(); |
| 433 } |
| 434 view.set('rmrelation_dialog', views.createModalPanel( |
| 435 'Are you sure you want to remove this relation? ' + |
| 436 'This cannot be undone.', |
| 437 '#rmrelation-modal-panel', |
| 438 'Remove Relation', |
| 439 Y.bind(function(ev) { |
| 440 ev.preventDefault(); |
| 441 var confirmButton = ev.target; |
| 442 confirmButton.set('disabled', true); |
| 443 view.removeRelation(d, context, view, confirmButton); |
| 444 }, |
| 445 this))); |
| 446 }, |
| 447 |
| 448 cancelRelationBuild: function() { |
| 449 var topo = this.get('component'); |
| 450 var vis = topo.vis; |
| 451 if (this.dragline) { |
| 452 // Get rid of our drag line |
| 453 this.dragline.remove(); |
| 454 this.dragline = null; |
| 455 } |
| 456 this.clickAddRelation = null; |
| 457 this.set('currentServiceClickAction', 'toggleControlPanel'); |
| 458 topo.buildingRelation = false; |
| 459 topo.fire('show', { selection: vis.selectAll('.service') }); |
| 460 vis.selectAll('.service').classed('selectable-service', false); |
| 461 }, |
| 462 |
| 463 /** |
| 464 * An "add relation" action has been initiated by the user. |
| 465 * |
| 466 * @method startRelation |
| 467 * @param {object} service The service that is the source of the |
| 468 * relation. |
| 469 * @return {undefined} Side effects only. |
| 470 */ |
| 471 startRelation: function(service) { |
| 472 // Set flags on the view that indicate we are building a relation. |
| 473 var topo = this.get('component'); |
| 474 var vis = topo.vis; |
| 475 |
| 476 topo.buildingRelation = true; |
| 477 this.clickAddRelation = true; |
| 478 |
| 479 topo.fire('show', { selection: vis.selectAll('.service') }); |
| 480 |
| 481 var db = this.get('component').get('db'); |
| 482 var getServiceEndpoints = this.get('component') |
| 483 .get('getServiceEndpoints'); |
| 484 var endpoints = models.getEndpoints( |
| 485 service, getServiceEndpoints(), db); |
| 486 // Transform endpoints into a list of relatable services (to the |
| 487 // service). |
| 488 var possible_relations = Y.Array.map( |
| 489 Y.Array.flatten(Y.Object.values(endpoints)), |
| 490 function(ep) {return ep.service;}); |
| 491 var invalidRelationTargets = {}; |
| 492 |
| 493 // Iterate services and invert the possibles list. |
| 494 db.services.each(function(s) { |
| 495 if (Y.Array.indexOf(possible_relations, |
| 496 s.get('id')) === -1) { |
| 497 invalidRelationTargets[s.get('id')] = true; |
| 498 } |
| 499 }); |
| 500 |
| 501 // Fade elements to which we can't relate. |
| 502 // Rather than two loops this marks |
| 503 // all services as selectable and then |
| 504 // removes the invalid ones. |
| 505 var sel = vis.selectAll('.service') |
| 506 .classed('selectable-service', true) |
| 507 .filter(function(d) { |
| 508 return (d.id in invalidRelationTargets && |
| 509 d.id !== service.id); |
| 510 }); |
| 511 topo.fire('fade', { selection: sel }); |
| 512 sel.classed('selectable-service', false); |
| 513 |
| 514 // Store possible endpoints. |
| 515 this.set('addRelationStart_possibleEndpoints', endpoints); |
| 516 // Set click action. |
| 517 this.set('currentServiceClickAction', 'ambiguousAddRelationCheck'); |
| 518 }, |
| 519 |
| 520 /* |
| 521 * Fired when clicking the first service in the add relation |
| 522 * flow. |
| 523 */ |
| 524 addRelationStart: function(m, view, context) { |
| 525 var topo = view.get('component'); |
| 526 var service = topo.serviceForBox(m); |
| 527 view.startRelation(service); |
| 528 // Store start service in attrs. |
| 529 view.set('addRelationStart_service', m); |
| 530 }, |
| 531 |
| 532 /* |
| 533 * Test if the pending relation is ambiguous. Display a menu if so, |
| 534 * create the relation if not. |
| 535 */ |
| 536 ambiguousAddRelationCheck: function(m, view, context) { |
| 537 var endpoints = view.get( |
| 538 'addRelationStart_possibleEndpoints')[m.id]; |
| 539 var container = view.get('container'); |
| 540 var topo = view.get('component'); |
| 541 |
| 542 if (endpoints && endpoints.length === 1) { |
| 543 // Create a relation with the only available endpoint. |
| 544 var ep = endpoints[0], |
| 545 endpoints_item = [ |
| 546 [ep[0].service, { |
| 547 name: ep[0].name, |
| 548 role: 'server' }], |
| 549 [ep[1].service, { |
| 550 name: ep[1].name, |
| 551 role: 'client' }]]; |
| 552 view.addRelationEnd(endpoints_item, view, context); |
| 553 return; |
| 554 } |
| 555 |
| 556 // Sort the endpoints alphabetically by relation name. |
| 557 endpoints = endpoints.sort(function(a, b) { |
| 558 return a[0].name + a[1].name < b[0].name + b[1].name; |
| 559 }); |
| 560 |
| 561 // Stop rubberbanding on mousemove. |
| 562 view.clickAddRelation = null; |
| 563 |
| 564 // Display menu with available endpoints. |
| 565 var menu = container.one('#ambiguous-relation-menu'); |
| 566 if (menu.one('.menu')) { |
| 567 menu.one('.menu').remove(true); |
| 568 } |
| 569 |
| 570 menu.append(Templates |
| 571 .ambiguousRelationList({endpoints: endpoints})); |
| 572 |
| 573 // For each endpoint choice, bind an an event to 'click' to |
| 574 // add the specified relation. |
| 575 menu.all('li').on('click', function(evt) { |
| 576 if (evt.currentTarget.hasClass('cancel')) { |
| 577 return; |
| 578 } |
| 579 var el = evt.currentTarget, |
| 580 endpoints_item = [ |
| 581 [el.getData('startservice'), { |
| 582 name: el.getData('startname'), |
| 583 role: 'server' }], |
| 584 [el.getData('endservice'), { |
| 585 name: el.getData('endname'), |
| 586 role: 'client' }]]; |
| 587 menu.removeClass('active'); |
| 588 view.addRelationEnd(endpoints_item, view, context); |
| 589 }); |
| 590 |
| 591 // Add a cancel item. |
| 592 menu.one('.cancel').on('click', function(evt) { |
| 593 menu.removeClass('active'); |
| 594 view.cancelRelationBuild(); |
| 595 }); |
| 596 |
| 597 // Display the menu at the service endpoint. |
| 598 var tr = topo.zoom.translate(); |
| 599 var z = topo.zoom.scale(); |
| 600 menu.setStyle('top', m.y * z + tr[1]); |
| 601 menu.setStyle('left', m.x * z + m.w * z + tr[0]); |
| 602 menu.addClass('active'); |
| 603 topo.set('active_service', m); |
| 604 topo.set('active_context', context); |
| 605 |
| 606 // Firing resized will ensure the menu's positioned properly. |
| 607 topo.fire('resized'); |
| 608 }, |
| 609 |
| 610 /* |
| 611 * Fired when clicking the second service is clicked in the |
| 612 * add relation flow. |
| 613 * |
| 614 * :param endpoints: array of two endpoints, each in the form |
| 615 * ['service name', { |
| 616 * name: 'endpoint type', |
| 617 * role: 'client or server' |
| 618 * }] |
| 619 */ |
| 620 addRelationEnd: function(endpoints, view, context) { |
| 621 // Redisplay all services |
| 622 view.cancelRelationBuild(); |
| 623 |
| 624 // Get the vis, and links, build the new relation. |
| 625 var vis = view.get('component').vis; |
| 626 var env = view.get('component').get('env'); |
| 627 var db = view.get('component').get('db'); |
| 628 var source = view.get('addRelationStart_service'); |
| 629 var relation_id = 'pending-' + endpoints[0][0] + endpoints[1][0]; |
| 630 |
| 631 if (endpoints[0][0] === endpoints[1][0]) { |
| 632 view.set('currentServiceClickAction', 'toggleControlPanel'); |
| 633 return; |
| 634 } |
| 635 |
| 636 // Create a pending relation in the database between the |
| 637 // two services. |
| 638 db.relations.create({ |
| 639 relation_id: relation_id, |
| 640 display_name: 'pending', |
| 641 endpoints: endpoints, |
| 642 pending: true |
| 643 }); |
| 644 |
| 645 // Firing the update event on the db will properly redraw the |
| 646 // graph and reattach events. |
| 647 //db.fire('update'); |
| 648 view.get('component').bindAllD3Events(); |
| 649 view.update(); |
| 650 |
| 651 // Fire event to add relation in juju. |
| 652 // This needs to specify interface in the future. |
| 653 env.add_relation( |
| 654 endpoints[0][0] + ':' + endpoints[0][1].name, |
| 655 endpoints[1][0] + ':' + endpoints[1][1].name, |
| 656 Y.bind(this._addRelationCallback, this, view, relation_id) |
| 657 ); |
| 658 view.set('currentServiceClickAction', 'toggleControlPanel'); |
| 659 }, |
| 660 |
| 661 _addRelationCallback: function(view, relation_id, ev) { |
| 662 console.log('addRelationCallback reached'); |
| 663 var topo = view.get('component'); |
| 664 var db = topo.get('db'); |
| 665 var vis = topo.vis; |
| 666 // Remove our pending relation from the DB, error or no. |
| 667 db.relations.remove( |
| 668 db.relations.getById(relation_id)); |
| 669 vis.select('#' + relation_id).remove(); |
| 670 if (ev.err) { |
| 671 db.notifications.add( |
| 672 new models.Notification({ |
| 673 title: 'Error adding relation', |
| 674 message: 'Relation ' + ev.endpoint_a + |
| 675 ' to ' + ev.endpoint_b, |
| 676 level: 'error' |
| 677 }) |
| 678 ); |
| 679 } else { |
| 680 // Create a relation in the database between the two services. |
| 681 var result = ev.result; |
| 682 var endpoints = Y.Array.map(result.endpoints, function(item) { |
| 683 var id = Y.Object.keys(item)[0]; |
| 684 return [id, item[id]]; |
| 685 }); |
| 686 db.relations.create({ |
| 687 relation_id: ev.result.id, |
| 688 type: result['interface'], |
| 689 endpoints: endpoints, |
| 690 pending: false, |
| 691 scope: result.scope, |
| 692 // endpoints[1][1].name should be the same |
| 693 display_name: endpoints[0][1].name |
| 694 }); |
| 695 } |
| 696 // Redraw the graph and reattach events. |
| 697 //db.fire('update'); |
| 698 view.get('component').bindAllD3Events(); |
| 699 view.update(); |
| 700 }, |
| 701 |
| 702 /* |
| 703 * Utility function to get subordinate relations for a service. |
| 704 */ |
| 705 subordinateRelationsForService: function(service) { |
| 706 return this.relPairs.filter(function(p) { |
| 707 return p.modelIds().indexOf(service.modelId()) !== -1 && |
| 708 p.scope === 'container'; |
| 709 }); |
| 710 }, |
| 711 |
| 712 subRelBlockMouseEnter: function(d, self) { |
| 713 // Add an 'active' class to all of the subordinate relations |
| 714 // belonging to this service. |
| 715 self.subordinateRelationsForService(d) |
| 716 .forEach(function(p) { |
| 717 utils.addSVGClass('#' + p.id, 'active'); |
| 718 }); |
| 719 }, |
| 720 |
| 721 subRelBlockMouseLeave: function(d, self) { |
| 722 // Remove 'active' class from all subordinate relations. |
| 723 if (!self.keepSubRelationsVisible) { |
| 724 utils.removeSVGClass('.subordinate-rel-group', 'active'); |
| 725 } |
| 726 }, |
| 727 |
| 728 /** |
| 729 * Toggle the visibility of subordinate relations for visibility |
| 730 * or removal. |
| 731 * @param {object} d The data-bound object (the subordinate). |
| 732 * @param {object} self The view. |
| 733 **/ |
| 734 subRelBlockClick: function(d, self) { |
| 735 if (self.keepSubRelationsVisible) { |
| 736 self.hideSubordinateRelations(); |
| 737 } else { |
| 738 self.showSubordinateRelations(this); |
| 739 } |
| 740 }, |
| 741 |
| 742 /** |
| 743 * Show subordinate relations for a service. |
| 744 * |
| 745 * @method showSubordinateRelations |
| 746 * @param {Object} subordinate The sub-rel-block g element in the form |
| 747 * of a DOM node. |
| 748 * @return {undefined} nothing. |
| 749 */ |
| 750 showSubordinateRelations: function(subordinate) { |
| 751 this.keepSubRelationsVisible = true; |
| 752 utils.addSVGClass(Y.one(subordinate).one('.sub-rel-count'), 'active'); |
| 753 }, |
| 754 |
| 755 /** |
| 756 * Hide subordinate relations. |
| 757 * |
| 758 * @method hideSubordinateRelations |
| 759 * @return {undefined} nothing. |
| 760 */ |
| 761 hideSubordinateRelations: function() { |
| 762 var container = this.get('container'); |
| 763 utils.removeSVGClass('.subordinate-rel-group', 'active'); |
| 764 this.keepSubRelationsVisible = false; |
| 765 utils.removeSVGClass(container.one('.sub-rel-count.active'), |
| 766 'active'); |
| 767 }, |
| 768 |
| 769 relationClick: function(d, self) { |
| 770 if (d.scope === 'container') { |
| 771 var subRelDialog = views.createModalPanel( |
| 772 'You may not remove a subordinate relation.', |
| 773 '#rmsubrelation-modal-panel'); |
| 774 subRelDialog.addButton( |
| 775 { value: 'Cancel', |
| 776 section: Y.WidgetStdMod.FOOTER, |
| 777 /** |
| 778 * @method action Hides the dialog on click. |
| 779 * @param {object} e The click event. |
| 780 * @return {undefined} nothing. |
| 781 */ |
| 782 action: function(e) { |
| 783 e.preventDefault(); |
| 784 subRelDialog.hide(); |
| 785 subRelDialog.destroy(); |
| 786 }, |
| 787 classNames: ['btn'] |
| 788 }); |
| 789 subRelDialog.get('boundingBox').all('.yui3-button') |
| 790 .removeClass('yui3-button'); |
| 791 } else { |
| 792 self.removeRelationConfirm(d, this, self); |
| 793 } |
26 } | 794 } |
27 | 795 |
28 }, { | 796 }, { |
29 ATTRS: {} | 797 ATTRS: {} |
30 | 798 |
31 }); | 799 }); |
32 views.RelationModule = RelationModule; | 800 views.RelationModule = RelationModule; |
33 }, '0.1.0', { | 801 }, '0.1.0', { |
34 requires: [ | 802 requires: [ |
35 'd3', | 803 'd3', |
36 'd3-components', | 804 'd3-components', |
37 'node', | 805 'node', |
38 'event', | 806 'event', |
39 'juju-models', | 807 'juju-models', |
40 'juju-env' | 808 'juju-env' |
41 ] | 809 ] |
42 }); | 810 }); |
OLD | NEW |