Merge lp:~rharding/juju-gui/renderBundle2 into lp:juju-gui/experimental

Proposed by Richard Harding
Status: Work in progress
Proposed branch: lp:~rharding/juju-gui/renderBundle2
Merge into: lp:juju-gui/experimental
Diff against target: 623 lines (+496/-12)
8 files modified
app/app.js (+1/-0)
app/assets/javascripts/d3-components.js (+3/-0)
app/modules-debug.js (+3/-0)
app/views/topology/bundle.js (+390/-0)
app/views/topology/relation.js (+3/-0)
app/views/topology/topology.js (+15/-12)
test/data/wp-deployer.yaml (+41/-0)
test/test_topology.js (+40/-0)
To merge this branch: bzr merge lp:~rharding/juju-gui/renderBundle2
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+182735@code.launchpad.net

Description of the change

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) wrote :

Reviewers: mp+182735_code.launchpad.net,

Message:
In general this looks good. Couple of nit-picks below. I'm not
comfortable with so much code dupe between bundle.js and service.js
though so not ok'ing all the way.

https://codereview.appspot.com/13361043/diff/1/app/assets/javascripts/d3-components.js
File app/assets/javascripts/d3-components.js (right):

https://codereview.appspot.com/13361043/diff/1/app/assets/javascripts/d3-components.js#newcode290
app/assets/javascripts/d3-components.js:290: if (this.get('interactive')
=== false) { return; }
what is the interactive attr? I don't see it defined and not following
what this is doing.

https://codereview.appspot.com/13361043/diff/1/app/views/topology/bundle.js
File app/views/topology/bundle.js (right):

https://codereview.appspot.com/13361043/diff/1/app/views/topology/bundle.js#newcode80
app/views/topology/bundle.js:80: //Process any changed data.
space after the //

https://codereview.appspot.com/13361043/diff/1/app/views/topology/bundle.js#newcode87
app/views/topology/bundle.js:87: // enter
enter? more or remove?

https://codereview.appspot.com/13361043/diff/1/app/views/topology/bundle.js#newcode92
app/views/topology/bundle.js:92: return (d.subordinate ? 'subordinate '
: '') +
so is this repeated? Can it be shared? A typology.utils helper or
something?

https://codereview.appspot.com/13361043/diff/1/test/test_topology.js
File test/test_topology.js (right):

https://codereview.appspot.com/13361043/diff/1/test/test_topology.js#newcode135
test/test_topology.js:135: // The size of the element shoul reflect teh
passed in params
the

Description:
WIP

https://code.launchpad.net/~rharding/juju-gui/renderBundle2/+merge/182735

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/13361043/

Affected files:
   A [revision details]
   M app/app.js
   M app/assets/javascripts/d3-components.js
   M app/modules-debug.js
   A app/views/topology/bundle.js
   M app/views/topology/relation.js
   M app/views/topology/topology.js
   A test/data/wp-deployer.yaml
   M test/test_topology.js

Revision history for this message
Madison Scott-Clary (makyo) wrote :

LGTM, QA okay

https://codereview.appspot.com/13361043/diff/1/app/views/topology/bundle.js
File app/views/topology/bundle.js (right):

https://codereview.appspot.com/13361043/diff/1/app/views/topology/bundle.js#newcode38
app/views/topology/bundle.js:38: Manage service rendering and events.
s/service/bundle

https://codereview.appspot.com/13361043/diff/1/test/test_topology.js
File test/test_topology.js (right):

https://codereview.appspot.com/13361043/diff/1/test/test_topology.js#newcode135
test/test_topology.js:135: // The size of the element shoul reflect teh
passed in params
On 2013/08/28 19:47:30, rharding wrote:
> the

should

https://codereview.appspot.com/13361043/

Unmerged revisions

988. By Richard Harding

Sync

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/app.js'
2--- app/app.js 2013-08-28 00:28:12 +0000
3+++ app/app.js 2013-08-28 19:30:33 +0000
4@@ -1507,6 +1507,7 @@
5 'FileSaver',
6 'juju-inspector-widget',
7 'juju-ghost-inspector',
8+ 'juju-view-bundle',
9 'viewmode-controls'
10 ]
11 });
12
13=== modified file 'app/assets/javascripts/d3-components.js'
14--- app/assets/javascripts/d3-components.js 2013-08-08 22:36:30 +0000
15+++ app/assets/javascripts/d3-components.js 2013-08-28 19:30:33 +0000
16@@ -132,6 +132,7 @@
17
18 modEvents = module.events;
19 this.events[module.name] = modEvents;
20+
21 this.bind(module.name);
22 module.componentBound();
23
24@@ -286,6 +287,7 @@
25 * @method bind
26 **/
27 bind: function(moduleName) {
28+ if (this.get('interactive') === false) { return; }
29 var eventSet = this.events,
30 filtered = {};
31
32@@ -319,6 +321,7 @@
33 owns = Y.Object.owns,
34 module;
35
36+ if (this.get('interactive') === false) { return; }
37 if (!modEvents || !modEvents.d3) {
38 return;
39 }
40
41=== modified file 'app/modules-debug.js'
42--- app/modules-debug.js 2013-08-19 15:53:41 +0000
43+++ app/modules-debug.js 2013-08-28 19:30:33 +0000
44@@ -226,6 +226,9 @@
45 fullpath: '/juju-ui/views/topology/topology.js'
46 },
47
48+ 'juju-view-bundle': {
49+ fullpath: '/juju-ui/views/topology/bundle.js'
50+ },
51 'juju-view-utils': {
52 fullpath: '/juju-ui/views/utils.js'
53 },
54
55=== added file 'app/views/topology/bundle.js'
56--- app/views/topology/bundle.js 1970-01-01 00:00:00 +0000
57+++ app/views/topology/bundle.js 2013-08-28 19:30:33 +0000
58@@ -0,0 +1,390 @@
59+/*
60+This file is part of the Juju GUI, which lets users view and manage Juju
61+environments within a graphical interface (https://launchpad.net/juju-gui).
62+Copyright (C) 2012-2013 Canonical Ltd.
63+
64+This program is free software: you can redistribute it and/or modify it under
65+the terms of the GNU Affero General Public License version 3, as published by
66+the Free Software Foundation.
67+
68+This program is distributed in the hope that it will be useful, but WITHOUT
69+ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
70+SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
71+General Public License for more details.
72+
73+You should have received a copy of the GNU Affero General Public License along
74+with this program. If not, see <http://www.gnu.org/licenses/>.
75+*/
76+
77+'use strict';
78+
79+/**
80+ * Provide the BundleTopology class.
81+ *
82+ * @module views
83+ * @submodule views.BundleTopology
84+ */
85+
86+YUI.add('juju-view-bundle', function(Y) {
87+
88+ var juju = Y.namespace('juju'),
89+ views = Y.namespace('juju.views'),
90+ utils = Y.namespace('juju.views.utils'),
91+ models = Y.namespace('juju.models'),
92+ d3ns = Y.namespace('d3'),
93+ topoUtils = Y.namespace('juju.topology.utils');
94+
95+ /**
96+ Manage service rendering and events.
97+
98+ @class BundleModule
99+ */
100+
101+ var BundleModule = Y.Base.create('BundleModule', d3ns.Module, [], {
102+ /**
103+ Sync view models with current db.models.
104+
105+ @method updateData
106+ */
107+ updateData: function() {
108+ //model data
109+ var topo = this.get('component');
110+ var vis = topo.vis;
111+ var db = topo.get('db');
112+ var store = topo.get('store');
113+
114+ var visibleServices = db.services.visible();
115+ views.toBoundingBoxes(this, visibleServices, topo.service_boxes, store);
116+ // Break a reference cycle that results in uncollectable objects leaking.
117+ visibleServices.reset();
118+
119+ // Nodes are mapped by modelId tuples.
120+ this.node = vis.selectAll('.service')
121+ .data(Y.Object.values(topo.service_boxes),
122+ function(d) {return d.modelId;});
123+ },
124+
125+
126+ /**
127+ Attempt to reuse as much of the existing graph and view models
128+ as possible to re-render the graph.
129+
130+ @method update
131+ */
132+ update: function() {
133+ var self = this,
134+ topo = this.get('component'),
135+ width = topo.get('width'),
136+ height = topo.get('height');
137+
138+ //Process any changed data.
139+ this.updateData();
140+
141+ // Generate a node for each service, draw it as a rect with
142+ // labels for service and charm.
143+ var node = this.node;
144+
145+ // enter
146+ node
147+ .enter().append('g')
148+ .attr({
149+ 'class': function(d) {
150+ return (d.subordinate ? 'subordinate ' : '') +
151+ (d.pending ? 'pending ' : '') + 'service';
152+ },
153+ 'transform': function(d) { return d.translateStr;}})
154+ .call(self.createServiceNode, self);
155+
156+ // Update all nodes.
157+ self.updateServiceNodes(node);
158+ },
159+
160+ /**
161+ Fill a service node with empty structures that will be filled out
162+ in the update stage.
163+
164+ @param {object} node the node to construct.
165+ @param {object} self reference to the view instance.
166+ @return {null} side effects only.
167+ @method createServiceNode
168+ */
169+ createServiceNode: function(node, self) {
170+ node.append('image')
171+ .classed('service-icon', true)
172+ .attr({
173+ 'xlink:href': function(d) {
174+ return d.icon;
175+ },
176+ width: 96,
177+ height: 96
178+ });
179+ node.append('text').append('tspan')
180+ .attr('class', 'name')
181+ .text(function(d) {return d.displayName; });
182+ },
183+
184+ /**
185+ Fill the empty structures within a service node such that they
186+ match the db.
187+
188+ @param {object} node the collection of nodes to update.
189+ @return {null} side effects only.
190+ @method updateServiceNodes
191+ */
192+ updateServiceNodes: function(node) {
193+ if (node.empty()) {
194+ return;
195+ }
196+ var self = this,
197+ topo = this.get('component'),
198+ landscape = topo.get('landscape');
199+
200+ // Apply Position Annotations
201+ // This is done after the services_boxes
202+ // binding as the event handler will
203+ // use that index.
204+ node.each(function(d) {
205+ var service = d.model,
206+ annotations = service.get('annotations'),
207+ x, y;
208+
209+ if (!annotations) {
210+ return;
211+ }
212+
213+ // If there are x/y annotations on the service model and they are
214+ // different from the node's current x/y coordinates, update the
215+ // node, as the annotations may have been set in another session.
216+ x = annotations['gui-x'];
217+ y = annotations['gui-y'];
218+ if (!d ||
219+ (x !== undefined && x !== d.x) ||
220+ (y !== undefined && y !== d.y)) {
221+ d.x = x;
222+ d.y = y;
223+ d3.select(this).attr({
224+ x: x,
225+ y: y,
226+ transform: d.translateStr});
227+
228+ }});
229+
230+ // Mark subordinates as such. This is needed for when a new service
231+ // is created.
232+ node.filter(function(d) {
233+ return d.subordinate;
234+ }).classed('subordinate', true);
235+
236+ // Size the node for drawing.
237+ node.attr({
238+ 'width': function(box) { box.w = 96; return box.w;},
239+ 'height': function(box) { box.h = 96; return box.h;}
240+ });
241+
242+ // Draw a subordinate relation indicator.
243+ var subRelationIndicator = node.filter(function(d) {
244+ return d.subordinate &&
245+ d3.select(this)
246+ .select('.sub-rel-block').empty();
247+ })
248+ .append('g')
249+ .attr('class', 'sub-rel-block')
250+ .attr('transform', function(d) {
251+ // Position the block so that the relation indicator will
252+ // appear at the right connector.
253+ return 'translate(' + [d.w, d.h / 2 - 26] + ')';
254+ });
255+
256+ subRelationIndicator.append('image')
257+ .attr({'xlink:href': '/juju-ui/assets/svgs/sub_relation.svg',
258+ 'width': 87,
259+ 'height': 47});
260+ subRelationIndicator.append('text').append('tspan')
261+ .attr({'class': 'sub-rel-count',
262+ 'x': 64,
263+ 'y': 47 * 0.8});
264+
265+ // The following are sizes in pixels of the SVG assets used to
266+ // render a service, and are used to in calculating the vertical
267+ // positioning of text down along the service block.
268+ var service_height = 224,
269+ name_size = 22,
270+ charm_label_size = 16,
271+ name_padding = 26,
272+ charm_label_padding = 150;
273+
274+ node.select('.name')
275+ .attr({'style': function(d) {
276+ // Programmatically size the font.
277+ // Number derived from service assets:
278+ // font-size 22px when asset is 224px.
279+ return 'font-size:' + d.h *
280+ (name_size / service_height) + 'px';
281+ },
282+ 'x': function(d) { return d.w / 2; },
283+ 'y': function(d) {
284+ // Number derived from service assets:
285+ // padding-top 26px when asset is 224px.
286+ return d.h * (name_padding / service_height) + d.h *
287+ (name_size / service_height) / 2;
288+ }
289+ });
290+
291+ // Show whether or not the service is exposed using an indicator.
292+ var exposed = node.filter(function(d) {
293+ return d.exposed;
294+ });
295+ exposed.each(function(d) {
296+ var existing = Y.one(this).one('.exposed-indicator');
297+ if (!existing) {
298+ existing = d3.select(this).append('image')
299+ .attr({'class': 'exposed-indicator on',
300+ 'xlink:href': '/juju-ui/assets/svgs/exposed.svg',
301+ 'width': 32,
302+ 'height': 32
303+ })
304+ .append('title')
305+ .text(function(d) {
306+ return d.exposed ? 'Exposed' : '';
307+ });
308+ }
309+ existing = d3.select(this).select('.exposed-indicator')
310+ .attr({
311+ 'x': 145,
312+ 'y': 79
313+ });
314+ });
315+ },
316+
317+
318+ /**
319+ Pans the environment view to the center all the services on the canvas.
320+
321+ @method panToCenter
322+ @param {object} evt The event fired.
323+ @return {undefined} Side effects only.
324+ */
325+ panToCenter: function(evt) {
326+ var topo = this.get('component');
327+ var vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes);
328+ this.findAndSetCentroid(vertices);
329+ },
330+
331+ /**
332+ Given a set of vertices, find the centroid and pan to that location.
333+
334+ @method findAndSetCentroid
335+ @param {array} vertices A list of vertices in the form [x, y].
336+ @return {undefined} Side effects only.
337+ */
338+ findAndSetCentroid: function(vertices) {
339+ var topo = this.get('component'),
340+ centroid = topoUtils.centroid(vertices);
341+ // The centroid is set on the topology object due to the fact that it is
342+ // used as a sigil to tell whether or not to pan to the point after the
343+ // first delta.
344+ topo.centroid = centroid;
345+ topo.fire('panToPoint', {point: topo.centroid});
346+ }
347+ }, {
348+ ATTRS: {
349+ /**
350+ @property {d3ns.Component} component
351+ */
352+ component: {}
353+ }
354+ });
355+ views.BundleModule = BundleModule;
356+
357+ /**
358+ Display a Bundle using an internal topology.
359+
360+ @class BundleTopology
361+ */
362+ function BundleTopology(options) {
363+ // Options and Init
364+ var self = this;
365+ options = options || {};
366+ this.options = options;
367+ this._cleanups = [];
368+ this.db = options.db;
369+ if (!this.db) {
370+ this.db = new models.Database();
371+ this._cleanups.push(this.db.destroy);
372+ }
373+ this.store = options.store;
374+ if (!this.store) {
375+ this.store = new juju.CharmWorld2({});
376+ this._cleanups.push(this.store.destroy);
377+ }
378+ this.container = options.container;
379+ if (!this.container) {
380+ this.container = Y.Node.create('<div>');
381+ this.container.addClass('topology-canvas');
382+ this._cleanups.push(function() {
383+ self.container.remove(true);
384+ });
385+ }
386+
387+ var topo = this.topology = new views.Topology();
388+ topo.setAttrs(Y.mix(options, {
389+ interactive: false,
390+ container: this.container,
391+ db: this.db,
392+ store: this.store
393+ }, true));
394+
395+ // Service view doesn't support Level Of Detail views.
396+ // BundleModule provides an icon centric view
397+ // of services till service module can support this directly.
398+ topo.addModule(views.BundleModule);
399+ topo.addModule(views.RelationModule);
400+ topo.addModule(views.PanZoomModule);
401+ }
402+
403+ BundleTopology.prototype.centerViewport = function(scale) {
404+ this.topology.modules.PanZoomModule._fire_zoom(scale);
405+ // Pan to the centroid of it all after the zoom
406+ this.panToCenter();
407+ return this;
408+ };
409+
410+ /**
411+ Pans the canvas to the center all the services.
412+
413+ @method panToCenter
414+ @param {object} evt The event fired.
415+ @return {undefined} Side effects only.
416+ */
417+ BundleTopology.prototype.panToCenter = function() {
418+ var topo = this.topology;
419+ var vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes);
420+ var centroid = topoUtils.centroid(vertices);
421+ this.topology.modules.PanZoomModule.panToPoint({point: centroid});
422+ };
423+
424+
425+ BundleTopology.prototype.render = function() {
426+ this.topology.render();
427+ this.centerViewport(0.66);
428+ return this;
429+ };
430+
431+ BundleTopology.prototype.destroy = function() {
432+ this._cleanups.forEach(function(cleanupFunc) {
433+ cleanupFunc();
434+ });
435+ };
436+
437+ views.BundleTopology = BundleTopology;
438+
439+}, '0.1.0', {
440+ requires: [
441+ 'd3',
442+ 'd3-components',
443+ 'juju-charm-store',
444+ 'juju-models',
445+ 'juju-topology',
446+ 'juju-view-utils'
447+ ]
448+});
449
450=== modified file 'app/views/topology/relation.js'
451--- app/views/topology/relation.js 2013-07-03 17:35:21 +0000
452+++ app/views/topology/relation.js 2013-08-28 19:30:33 +0000
453@@ -178,6 +178,9 @@
454 var db = topo.get('db');
455 var self = this;
456 var relations = db.relations.toArray();
457+ if (!relations || relations.length === 0) {
458+ return;
459+ }
460 this.relations = this.decorateRelations(relations);
461 this.updateLinks();
462 this.updateSubordinateRelationsCount();
463
464=== modified file 'app/views/topology/topology.js'
465--- app/views/topology/topology.js 2013-05-17 14:51:05 +0000
466+++ app/views/topology/topology.js 2013-08-28 19:30:33 +0000
467@@ -49,17 +49,15 @@
468 */
469 var Topology = Y.Base.create('Topology', d3ns.Component, [], {
470 initializer: function(options) {
471- Topology.superclass.constructor.apply(this, arguments);
472- this.options = Y.mix(options || {
473+ this.options = Y.mix(options || {}, {
474 minZoom: 0.25,
475 maxZoom: 2,
476 minSlider: 25,
477 maxSlider: 200
478 });
479-
480+ Topology.superclass.constructor.apply(this, arguments);
481 // Build a service.id -> BoundingBox map for services.
482 this.service_boxes = {};
483-
484 this._subscriptions = [];
485 },
486
487@@ -111,11 +109,16 @@
488 this.computeScales();
489
490 // Set up the visualization with a pack layout.
491- svg = d3.select(container.getDOMNode())
492- .selectAll('.topology-canvas')
493- .append('svg:svg')
494- .attr('width', width)
495- .attr('height', height);
496+ var canvas = d3.select(container.getDOMNode());
497+ var base = canvas.select('.topology-canvas');
498+ if (base.empty()) {
499+ base = canvas.append('div')
500+ .classed('topology-canvas', true);
501+ }
502+
503+ svg = base.append('svg:svg')
504+ .attr('width', width)
505+ .attr('height', height);
506 this.svg = svg;
507
508 this.zoomPlane = svg.append('rect')
509@@ -209,9 +212,9 @@
510 getter: function() {return this.get('size')[1];}
511 },
512 /*
513- * Scale and translate are managed by an external module
514- * (PanZoom in this case). If that module isn't
515- * loaded nothing will modify these values.
516+ Scale and translate are managed by an external module
517+ (PanZoom in this case). If that module isn't
518+ loaded nothing will modify these values.
519 */
520 scale: {
521 getter: function() {return this.zoom.scale();},
522
523=== added file 'test/data/wp-deployer.yaml'
524--- test/data/wp-deployer.yaml 1970-01-01 00:00:00 +0000
525+++ test/data/wp-deployer.yaml 2013-08-28 19:30:33 +0000
526@@ -0,0 +1,41 @@
527+envExport:
528+ series: precise
529+ services:
530+ mysql:
531+ charm: "cs:precise/mysql-27"
532+ num_units: 1
533+ options:
534+ "binlog-format": MIXED
535+ "block-size": "5"
536+ "dataset-size": "80%"
537+ flavor: distro
538+ 'gui-x': 50
539+ 'gui-y': 50
540+ "ha-bindiface": eth0
541+ "ha-mcastport": "5411"
542+ "max-connections": "-1"
543+ "preferred-storage-engine": InnoDB
544+ "query-cache-size": "-1"
545+ "query-cache-type": "OFF"
546+ "rbd-name": mysql1
547+ "tuning-level": safest
548+ vip: ""
549+ vip_cidr: "24"
550+ vip_iface: eth0
551+ annotations:
552+ "gui-x": 115
553+ "gui-y": 89
554+ wordpress:
555+ charm: "cs:precise/wordpress-16"
556+ num_units: 1
557+ options:
558+ debug: "no"
559+ engine: nginx
560+ tuning: single
561+ "wp-content": ""
562+ annotations:
563+ "gui-x": 510
564+ "gui-y": 184
565+ relations:
566+ - - "wordpress:db"
567+ - "mysql:db"
568
569=== modified file 'test/test_topology.js'
570--- test/test_topology.js 2013-06-05 13:27:07 +0000
571+++ test/test_topology.js 2013-08-28 19:30:33 +0000
572@@ -28,6 +28,7 @@
573 Y = YUI(GlobalConfig).use(['juju-topology',
574 'd3-components',
575 'juju-tests-utils',
576+ 'juju-view-bundle',
577 'node',
578 'node-event-simulate'],
579 function(Y) {
580@@ -111,4 +112,43 @@
581 Y.Lang.isValue(topo.vis).should.equal(true);
582 });
583
584+ it('should be able to create a bundle display topology', function(done) {
585+ container.destroy(true);
586+ container = utils.makeContainer();
587+
588+ db = new models.Database();
589+ db.environment.set('defaultSeries', 'precise');
590+ var fakeStore = utils.makeFakeStore(db.charms);
591+ fakeStore.iconpath = function() { return 'fake.svg'; };
592+
593+ db.importDeployer(
594+ jsyaml.safeLoad(utils.loadFixture('data/wp-deployer.yaml')),
595+ fakeStore, {useGhost: true, targetBundle: 'wordpress-prod'})
596+ .then(function() {
597+ // Init the topo with the db at this point and ...
598+ var bundle = new views.BundleTopology({
599+ container: container,
600+ size: [320, 240],
601+ db: db,
602+ store: fakeStore}).render();
603+
604+ // The size of the element shoul reflect teh passed in params
605+ var svg = d3.select(container.getDOMNode()).select('svg');
606+ assert.equal(svg.attr('width'), 320);
607+ assert.equal(svg.attr('height'), 240);
608+
609+ // We should have the two rendered services
610+ assert.equal(container.all('.service').size(), 2);
611+ var service = svg.select('.service');
612+ assert.equal(service.attr('width'), '96');
613+ assert.equal(service.attr('transform'), 'translate(115,89)');
614+ // and the one relation between them
615+ assert.equal(container.all('.relation').size(), 1);
616+
617+ container.remove(true);
618+ bundle.destroy();
619+ done();
620+ }).then(undefined, done);
621+ });
622+
623 });

Subscribers

People subscribed via source and target branches