yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #01717
[Merge] lp:~hazmat/juju-gui/component-modules into lp:juju-gui
Kapil Thangavelu has proposed merging lp:~hazmat/juju-gui/component-modules into lp:juju-gui.
Requested reviews:
Juju GUI Hackers (juju-gui)
For more details, see:
https://code.launchpad.net/~hazmat/juju-gui/component-modules/+merge/135501
Ben's branch for env refactoring
Ben's branch for env refactoring... testing
https://codereview.appspot.com/6842084/
--
https://code.launchpad.net/~hazmat/juju-gui/component-modules/+merge/135501
Your team Juju GUI Hackers is requested to review the proposed merge of lp:~hazmat/juju-gui/component-modules into lp:juju-gui.
=== modified file 'Makefile'
--- Makefile 2012-11-20 15:10:30 +0000
+++ Makefile 2012-11-21 19:26:21 +0000
@@ -97,7 +97,7 @@
yuidoc-lint: $(JSFILES)
bin/lint-yuidoc
-lint: gjslint jshint yuidoc-lint
+lint: gjslint jshint
virtualenv/bin/gjslint virtualenv/bin/fixjsstyle:
virtualenv virtualenv
=== modified file 'app/assets/javascripts/d3-components.js'
--- app/assets/javascripts/d3-components.js 2012-11-09 13:39:45 +0000
+++ app/assets/javascripts/d3-components.js 2012-11-21 19:26:21 +0000
@@ -16,23 +16,33 @@
* @property events
* @type {object}
**/
- events: {
+ _defaultEvents: {
scene: {},
d3: {},
yui: {}
},
+ events: {},
+
initializer: function() {
- this.events = Y.merge(this.events);
- }
+ this.events = Y.mix(this.events, this._defaultEvents,
+ false, undefined, 0, true);
+ },
+
+ componentBound: function() {},
+ render: function() {},
+ update: function() {}
}, {
ATTRS: {
component: {},
options: {},
- container: {getter: function() {
- return this.get('component').get('container');}}
- }
- });
+ container: {
+ getter: function() {
+ var component = this.get('component');
+ return component && component.get('container') || undefined;
+ }
+ }
+ }});
ns.Module = Module;
@@ -56,6 +66,8 @@
initializer: function() {
this.modules = {};
this.events = {};
+ // Used to track the renderOnce invocation.
+ this._rendered = false;
},
/**
@@ -82,6 +94,10 @@
module = ModClassOrInstance,
modEvents;
+ if (ModClassOrInstance === undefined) {
+ throw 'undefined Module in addModule call';
+ }
+
if (!(ModClassOrInstance instanceof Module)) {
module = new ModClassOrInstance();
}
@@ -94,6 +110,7 @@
modEvents = module.events;
this.events[module.name] = modEvents;
this.bind(module.name);
+ module.componentBound();
return this;
},
@@ -169,6 +186,14 @@
selector, handler, modName, result);
return;
}
+
+ // Set up binding context for callback.
+ result.context = module;
+ if (handler.context) {
+ if (handler.context === 'component') {
+ result.context = self;
+ }
+ }
return result;
}
@@ -193,15 +218,19 @@
Y.each(['after', 'before', 'on'], function(eventPhase) {
var resolvedHandler = {};
Y.each(modEvents.yui, function(handler, name) {
- handler = _normalizeHandler(handler, module);
+ handler = _normalizeHandler(handler, module, name);
if (!handler || handler.phase !== eventPhase) {
return;
}
- resolvedHandler[name] = handler.callback;
+ resolvedHandler[name] = handler;
}, this);
// Bind resolved event handlers as a group.
if (Y.Object.keys(resolvedHandler).length) {
- subscriptions.push(Y[eventPhase](resolvedHandler));
+ Y.each(resolvedHandler, function(handler, name) {
+ subscriptions.push(Y[eventPhase](name,
+ handler.callback,
+ handler.context));
+ });
}
});
}
@@ -320,10 +349,20 @@
},
/**
+ * @method renderOnce
+ *
+ * Called the first time render is invoked. See {render}.
+ **/
+ renderOnce: function() {},
+
+ /**
* @method render
* @chainable
*
- * Render each module bound to the canvas
+ * Render each module bound to the canvas. The first call to
+ * render() will automatically call renderOnce (a noop by default)
+ * and update(). If update requires some render state to operate on
+ * renderOnce is the place to include that setup code.
*/
render: function() {
var self = this;
@@ -337,6 +376,12 @@
// If the container isn't bound to the DOM
// do so now.
this.attachContainer();
+ if (!this._rendered) {
+ self.renderOnce();
+ self.update();
+ self._rendered = true;
+ }
+
// Render modules.
Y.each(this.modules, renderAndBind, this);
return this;
=== modified file 'app/modules-debug.js'
--- app/modules-debug.js 2012-11-15 15:44:00 +0000
+++ app/modules-debug.js 2012-11-21 19:26:21 +0000
@@ -37,6 +37,32 @@
},
// Views
+ 'juju-topology-relation': {
+ fullpath: '/juju-ui/views/topology/relation.js'
+ },
+
+ 'juju-topology-panzoom': {
+ fullpath: '/juju-ui/views/topology/panzoom.js'
+ },
+
+ 'juju-topology-viewport': {
+ fullpath: '/juju-ui/views/topology/viewport.js'
+ },
+
+ 'juju-topology-service': {
+ fullpath: '/juju-ui/views/topology/service.js'
+ },
+
+ 'juju-topology': {
+ fullpath: '/juju-ui/views/topology/topology.js',
+ require: [
+ 'juju-topology-service',
+ 'juju-topology-relation',
+ 'juju-topology-panzoom',
+ 'juju-topology-viewport'
+ ]
+ },
+
'juju-view-utils': {
fullpath: '/juju-ui/views/utils.js'
},
@@ -79,6 +105,7 @@
'juju-templates',
'juju-notifications',
'juju-view-utils',
+ 'juju-topology',
'juju-view-environment',
'juju-view-service',
'juju-view-unit',
=== modified file 'app/templates/overview.handlebars'
--- app/templates/overview.handlebars 2012-11-15 15:55:06 +0000
+++ app/templates/overview.handlebars 2012-11-21 19:26:21 +0000
@@ -1,5 +1,5 @@
-<div id="overview">
- <div id="canvas" class="crosshatch-background">
+<div>
+ <div class="topology-canvas crosshatch-background">
<div class="environment-menu" id="service-menu">
<div class="triangle"> </div>
<ul>
=== modified file 'app/views/environment.js'
--- app/views/environment.js 2012-11-20 16:22:21 +0000
+++ app/views/environment.js 2012-11-21 19:26:21 +0000
@@ -2,252 +2,32 @@
/**
* Provides the main app class.
*
- * @module views
+ * @module environment
*/
YUI.add('juju-view-environment', function(Y) {
var views = Y.namespace('juju.views'),
utils = Y.namespace('juju.views.utils'),
- Templates = views.Templates,
- models = Y.namespace('juju.models');
+ models = Y.namespace('juju.models'),
+ Templates = views.Templates;
/**
* Display an environment.
*
* @class environment
- * @namespace views
+ * @namespace juju.views
*/
var EnvironmentView = Y.Base.create('EnvironmentView', Y.View,
- [views.JujuBaseView], {
- events: {
- '#zoom-out-btn': {click: 'zoom_out'},
- '#zoom-in-btn': {click: 'zoom_in'},
- '.graph-list-picker .picker-button': {
- click: 'showGraphListPicker'
- },
- '.graph-list-picker .picker-expanded': {
- click: 'hideGraphListPicker'
- },
- // Menu/Controls
- '.add-relation': {
- /** The user clicked on the "Build Relation" menu item. */
- click: function() {
- var box = this.get('active_service'),
- service = this.serviceForBox(box),
- context = this.get('active_context');
- this.addRelationDragStart(box, context);
- this.service_click_actions
- .toggleControlPanel(box, this, context);
- this.service_click_actions.addRelationStart(box, this, context);
- }
- },
- '.view-service': {
- /** The user clicked on the "View" menu item. */
- click: function() {
- // Get the service element
- var box = this.get('active_service'),
- service = this.serviceForBox(box);
- this.service_click_actions
- .toggleControlPanel(box, this);
- this.service_click_actions
- .show_service(service, this);
- }
- },
- '.destroy-service': {
- /** The user clicked on the "Destroy" menu item. */
- click: function() {
- // Get the service element
- var box = this.get('active_service'),
- service = this.serviceForBox(box);
- this.service_click_actions
- .toggleControlPanel(box, this);
- this.service_click_actions
- .destroyServiceConfirm(service, this);
- }
- }
- },
-
- sceneEvents: {
- // Service Related
- '.service': {
- click: 'serviceClick',
- dblclick: 'serviceDblClick',
- mouseenter: function(d, self) {
- var rect = Y.one(this);
- // Do not fire if this service isn't selectable.
- if (!self.hasSVGClass(rect, 'selectable-service')) {
- return;
- }
-
- // Do not fire unless we're within the service box.
- var container = self.get('container'),
- mouse_coords = d3.mouse(container.one('svg').getDOMNode());
- if (!d.containsPoint(mouse_coords, self.zoom)) {
- return;
- }
-
- // Do not fire if we're on the same service.
- if (d === self.get('addRelationStart_service')) {
- return;
- }
-
- self.set('potential_drop_point_service', d);
- self.set('potential_drop_point_rect', rect);
- self.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 (self.dragline) {
- var connectors = d.getConnectorPair(
- self.get('addRelationStart_service')),
- s = connectors[0],
- t = connectors[1];
- self.dragline.attr('x1', t[0])
- .attr('y1', t[1])
- .attr('x2', s[0])
- .attr('y2', s[1])
- .attr('class', 'relation pending-relation dragline');
- }
- },
- mouseleave: 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 container = self.get('container'),
- mouse_coords = d3.mouse(container.one('svg').getDOMNode());
- if (d.containsPoint(mouse_coords, self.zoom)) {
- return;
- }
- var rect = Y.one(this).one('.service-border');
- self.set('potential_drop_point_service', null);
- self.set('potential_drop_point_rect', null);
- self.removeSVGClass(rect, 'hover');
-
- if (self.dragline) {
- self.dragline.attr('class',
- 'relation pending-relation dragline dragging');
- }
- },
- mousemove: 'mousemove'
- },
- '.sub-rel-block': {
- mouseenter: function(d, self) {
- // Add an 'active' class to all of the subordinate relations
- // belonging to this service.
- self.subordinateRelationsForService(d)
- .forEach(function(p) {
- self.addSVGClass('#' + p.id, 'active');
- });
- },
- mouseleave: function(d, self) {
- // Remove 'active' class from all subordinate relations.
- if (!self.keepSubRelationsVisible) {
- self.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.
- */
- click: function(d, self) {
- if (self.keepSubRelationsVisible) {
- self.hideSubordinateRelations();
- } else {
- self.showSubordinateRelations(this);
- }
- }
- },
- '.service-status': {
- mouseover: function(d, self) {
- d3.select(this)
- .select('.unit-count')
- .attr('class', 'unit-count show-count');
- },
- mouseout: function(d, self) {
- d3.select(this)
- .select('.unit-count')
- .attr('class', 'unit-count hide-count');
- }
- },
-
- // Relation Related
- '.rel-label': {
- /** The user clicked on the relation label. */
- click: 'relationClick',
- mousemove: 'mousemove'
- },
-
- '#canvas rect:first-child': {
- /**
- * If the user clicks on the background we cancel any active add
- * relation.
- */
- click: function(d, self) {
- var container = self.get('container');
- container.all('.environment-menu.active').removeClass('active');
- self.service_click_actions.toggleControlPanel(null, self);
- self.cancelRelationBuild();
- self.hideSubordinateRelations();
- },
- mousemove: 'mousemove'
- },
- '.dragline': {
- /** The user clicked while the dragline was active. */
- click: function(d, self) {
- // It was technically the dragline that was clicked, but the
- // intent was to click on the background, so...
- self.backgroundClicked();
- }
- }
- },
-
- d3Events: {
- '.service': {
- 'mousedown.addrel': function(d, self) {
- var evt = d3.event;
- self.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
- self.addRelationDragStart(d, this);
- }, [d, evt], false);
- },
- 'mouseup.addrel': function(d, self) {
- // Cancel the long-click timer if it exists.
- if (self.longClickTimer) {
- self.longClickTimer.cancel();
- }
- }
- }
- },
-
+ [views.JujuBaseView],
+ {
initializer: function() {
console.log('View: Initialized: Env');
this.publish('navigateTo', {preventable: false});
-
- // Build a service.id -> BoundingBox map for services.
- this.service_boxes = {};
-
- // Track events bound to the canvas
- this._sceneEvents = [];
},
render: function() {
+<<<<<<< TREE
var container = this.get('container');
EnvironmentView.superclass.render.apply(this, arguments);
container.setHTML(Templates.overview());
@@ -1796,11 +1576,37 @@
// Redraw the graph and reattach events.
db.fire('update');
}
+=======
+ var container = this.get('container'),
+ topo;
+
+ //If we need the initial HTML template
+ // take care of that.
+ if (!this.svg) {
+ EnvironmentView.superclass.render.apply(this, arguments);
+ container.setHTML(Templates.overview());
+ this.svg = container.one('#overview');
+ }
+
+ if (!this.get('topo')) {
+ topo = new views.Topology();
+ topo.setAttrs({
+ size: [640, 480],
+ env: this.get('env'),
+ db: this.get('db'),
+ container: container});
+ // Bind all the behaviors we need as modules.
+ topo.addModule(views.ServiceModule);
+
+ this.set('topo', topo);
+ topo.update();
+ }
+ topo.render();
+ return this;
+>>>>>>> MERGE-SOURCE
}
-
}, {
ATTRS: {
- currentServiceClickAction: { value: 'toggleControlPanel' }
}
});
@@ -1810,12 +1616,12 @@
'juju-view-utils',
'juju-models',
'd3',
+ 'd3-components',
'base-build',
'handlebars-base',
'node',
'svg-layouts',
'event-resize',
'slider',
- 'slider-base',
'view']
});
=== added directory 'app/views/topology'
=== added file 'app/views/topology/panzoom.js'
--- app/views/topology/panzoom.js 1970-01-01 00:00:00 +0000
+++ app/views/topology/panzoom.js 2012-11-21 19:26:21 +0000
@@ -0,0 +1,42 @@
+'use strict';
+
+YUI.add('juju-topology-panzoom', function(Y) {
+ var views = Y.namespace('juju.views'),
+ models = Y.namespace('juju.models'),
+ d3ns = Y.namespace('d3');
+
+ /**
+ * @module topology-service
+ * @class Service
+ * @namespace juju.views
+ **/
+ var PanZoomModule = Y.Base.create('PanZoomModule', d3ns.Module, [], {
+ initializer: function(options) {
+ PanZoomModule.superclass.constructor.apply(this, arguments);
+ },
+
+ render: function() {
+ PanZoomModule.superclass.render.apply(this, arguments);
+ return this;
+ },
+
+ update: function() {
+ PanZoomModule.superclass.update.apply(this, arguments);
+ return this;
+ }
+
+ }, {
+ ATTRS: {}
+
+ });
+ views.PanZoomModule = PanZoomModule;
+}, '0.1.0', {
+ requires: [
+ 'd3',
+ 'd3-components',
+ 'node',
+ 'event',
+ 'juju-models',
+ 'juju-env'
+ ]
+});
=== added file 'app/views/topology/relation.js'
--- app/views/topology/relation.js 1970-01-01 00:00:00 +0000
+++ app/views/topology/relation.js 2012-11-21 19:26:21 +0000
@@ -0,0 +1,42 @@
+'use strict';
+
+YUI.add('juju-topology-relation', function(Y) {
+ var views = Y.namespace('juju.views'),
+ models = Y.namespace('juju.models'),
+ d3ns = Y.namespace('d3');
+
+ /**
+ * @module topology-service
+ * @class Service
+ * @namespace juju.views
+ **/
+ var RelationModule = Y.Base.create('RelationModule', d3ns.Module, [], {
+ initializer: function(options) {
+ RelationModule.superclass.constructor.apply(this, arguments);
+ },
+
+ render: function() {
+ RelationModule.superclass.render.apply(this, arguments);
+ return this;
+ },
+
+ update: function() {
+ RelationModule.superclass.update.apply(this, arguments);
+ return this;
+ }
+
+ }, {
+ ATTRS: {}
+
+ });
+ views.RelationModule = RelationModule;
+}, '0.1.0', {
+ requires: [
+ 'd3',
+ 'd3-components',
+ 'node',
+ 'event',
+ 'juju-models',
+ 'juju-env'
+ ]
+});
=== added file 'app/views/topology/service.js'
--- app/views/topology/service.js 1970-01-01 00:00:00 +0000
+++ app/views/topology/service.js 2012-11-21 19:26:21 +0000
@@ -0,0 +1,361 @@
+'use strict';
+
+YUI.add('juju-topology-service', function(Y) {
+ var views = Y.namespace('juju.views'),
+ models = Y.namespace('juju.models'),
+ d3ns = Y.namespace('d3');
+
+ /**
+ * @module topology-service
+ * @class Service
+ * @namespace juju.views
+ **/
+ var ServiceModule = Y.Base.create('ServiceModule', d3ns.Module, [], {
+ subordinate_margin: {
+ top: 0.05,
+ bottom: 0.1,
+ left: 0.084848,
+ right: 0.084848},
+
+ service_margin: {
+ top: 0,
+ bottom: 0.1667,
+ left: 0.086758,
+ right: 0.086758},
+
+ initializer: function(options) {
+ ServiceModule.superclass.constructor.apply(this, arguments);
+ // Mapping of serviceId to BoundingBox of service.
+ this.service_boxes = {};
+
+ },
+
+ componentBound: function() {
+ var component = this.get('component');
+ //component.on('sizeChange', this._scaleLayout);
+ this._scaleLayout();
+ this._buildDrag();
+ },
+
+ _scaleLayout: function() {
+ this.layout = d3.layout.pack()
+ .size(this.get('component').get('size'))
+ .value(function(d) {return d.unit_count;})
+ .padding(300);
+ },
+
+ _buildDrag: function() {
+ var container = this.get('component'),
+ self = this;
+
+ this.drag = d3.behavior.drag()
+ .on('dragstart', function(d) {
+ d.oldX = d.x;
+ d.oldY = d.y;
+ Y.one(container).all('.environment-menu.active')
+ .removeClass('active');
+ })
+ .on('drag', function(d, i) {
+ if (self.longClickTimer) {
+ self.longClickTimer.cancel();
+ }
+ d.x += d3.event.dx;
+ d.y += d3.event.dy;
+ d3.select(this).attr('transform', function(d, i) {
+ return d.translateStr();
+ });
+ Y.one(container).all('.environment-menu.active')
+ .removeClass('active');
+ });
+ },
+
+ render: function() {
+ var topology = this.get('component');
+
+ ServiceModule.superclass.render.apply(this, arguments);
+
+ // Enter
+ this.serviceSelection
+ .enter().append('g')
+ .call(this.drag)
+ .attr('class', function(d) {
+ return (d.subordinate ? 'subordinate ' : '') + 'service';
+ })
+ .attr('transform', function(d) { return d.translateStr();});
+
+ // Update.
+ this.drawService(this.serviceSelection);
+
+ // Exit.
+ this.serviceSelection.exit()
+ .remove();
+
+ return this;
+ },
+
+ update: function() {
+ ServiceModule.superclass.update.apply(this, arguments);
+
+ var topology = this.get('component'),
+ db = topology.get('db'),
+ services = db.services.map(views.toBoundingBox),
+ new_services = Y.Object.values(this.service_boxes)
+ .filter(function(boundingBox) {
+ return !Y.Lang.isNumber(boundingBox.x);
+ });
+
+ // Layout new nodes.
+ this.layout
+ .nodes({children: new_services});
+
+ Y.each(services, function(service_box) {
+ var existing = this.service_boxes[service_box.id];
+ if (existing) {
+ service_box.pos = existing.pos;
+ }
+ service_box.margins(service_box.subordinate ?
+ this.subordinate_margin :
+ this.service_margin);
+
+ this.service_boxes[service_box.id] = service_box;
+ }, this);
+
+ this.serviceSelection = topology.vis.selectAll('.service')
+ .data(services, function(d) {
+ return d.modelId();});
+
+ return this;
+ },
+
+ drawService: function(node) {
+ var self = this,
+ topology = this.get('component'),
+ service_scale = topology.service_scale,
+ service_scale_width = topology.service_scale_width,
+ service_scale_height = topology.service_scale_height;
+
+ // Size the node for drawing.
+ node
+ .attr('width', function(d) {
+ // NB: if a service has zero units, as is possible with
+ // subordinates, then default to 1 for proper scaling, as
+ // a value of 0 will return a scale of 0 (this does not
+ // affect the unit count, just the scale of the service).
+ var w = service_scale(d.unit_count || 1);
+ d.w = w;
+ return w;
+ })
+ .attr('height', function(d) {
+ var h = service_scale(d.unit_count || 1);
+ d.h = h;
+ return h;
+ });
+
+ // Draw subordinate services.
+ node.filter(function(d) { return d.subordinate; })
+ .append('image')
+ .attr('xlink:href', '/juju-ui/assets/svgs/sub_module.svg')
+ .attr('width', function(d) { return d.w; })
+ .attr('height', function(d) { return d.h; });
+
+ // Draw a subordinate relation indicator.
+ var sub_relation = node.filter(function(d) {
+ return d.subordinate;
+ })
+ .append('g')
+ .attr('class', 'sub-rel-block')
+ .attr('transform', function(d) {
+ // Position the block so that the relation indicator will
+ // appear at the right connector.
+ return 'translate(' + [d.w, d.h / 2 - 26] + ')';
+ });
+
+ sub_relation.append('image')
+ .attr('xlink:href', '/juju-ui/assets/svgs/sub_relation.svg')
+ .attr('width', 87)
+ .attr('height', 47);
+ sub_relation.append('text').append('tspan')
+ .attr('class', 'sub-rel-count')
+ .attr('x', 64)
+ .attr('y', 47 * 0.8)
+ .text(function(d) {
+ return views.subordinateRelationsForService(
+ d, self.modules.relations.rel_pairs).length;
+ });
+ // Draw non-subordinate services services
+ node.filter(function(d) {
+ return !d.subordinate;
+ })
+ .append('image')
+ .attr('xlink:href', '/juju-ui/assets/svgs/service_module.svg')
+ .attr('width', function(d) {
+ return d.w;
+ })
+ .attr('height', function(d) {
+ return d.h;
+ });
+
+ // The following are sizes in pixels of the SVG assets used to
+ // render a service, and are used to in calculating the vertical
+ // positioning of text down along the service block.
+ var service_height = 224,
+ name_size = 22,
+ charm_label_size = 16,
+ name_padding = 26,
+ charm_label_padding = 118;
+
+ var service_labels = node.append('text').append('tspan')
+ .attr('class', 'name')
+ .attr('style', function(d) {
+ // Programmatically size the font.
+ // Number derived from service assets:
+ // font-size 22px when asset is 224px.
+ return 'font-size:' + d.h *
+ (name_size / service_height) + 'px';
+ })
+ .attr('x', function(d) {
+ return d.w / 2;
+ })
+ .attr('y', function(d) {
+ // Number derived from service assets:
+ // padding-top 26px when asset is 224px.
+ return d.h * (name_padding / service_height) + d.h *
+ (name_size / service_height) / 2;
+ })
+ .text(function(d) {return d.id; });
+
+ var charm_labels = node.append('text').append('tspan')
+ .attr('class', 'charm-label')
+ .attr('style', function(d) {
+ // Programmatically size the font.
+ // Number derived from service assets:
+ // font-size 16px when asset is 224px.
+ return 'font-size:' + d.h *
+ (charm_label_size / service_height) + 'px';
+ })
+ .attr('x', function(d) {
+ return d.w / 2;
+ })
+ .attr('y', function(d) {
+ // Number derived from service assets:
+ // padding-top: 118px when asset is 224px.
+ return d.h * (charm_label_padding / service_height) - d.h *
+ (charm_label_size / service_height) / 2;
+ })
+ .attr('dy', '3em')
+ .text(function(d) { return d.charm; });
+
+ // Show whether or not the service is exposed using an
+ // indicator (currently a simple circle).
+ // TODO this will likely change to an image with UI uodates.
+ var exposed_indicator = node.filter(function(d) {
+ return d.exposed;
+ })
+ .append('image')
+ .attr('xlink:href', '/juju-ui/assets/svgs/exposed.svg')
+ .attr('width', function(d) {
+ return d.w / 6;
+ })
+ .attr('height', function(d) {
+ return d.w / 6;
+ })
+ .attr('x', function(d) {
+ return d.w / 10 * 7;
+ })
+ .attr('y', function(d) {
+ return d.getRelativeCenter()[1] - (d.w / 6) / 2;
+ })
+ .attr('class', 'exposed-indicator on');
+ exposed_indicator.append('title')
+ .text(function(d) {
+ return d.exposed ? 'Exposed' : '';
+ });
+
+ // Add the relative health of a service in the form of a pie chart
+ // comprised of units styled appropriately.
+ var status_chart_arc = d3.svg.arc()
+ .innerRadius(0)
+ .outerRadius(function(d) {
+ // Make sure it's exactly as wide as the mask
+ return parseInt(
+ d3.select(this.parentNode)
+ .select('image')
+ .attr('width'), 10) / 2;
+ });
+
+ var status_chart_layout = d3.layout.pie()
+ .value(function(d) { return (d.value ? d.value : 1); })
+ .sort(function(a, b) {
+ // Ensure that the service health graphs will be renders in
+ // the correct order: error - pending - running.
+ var states = {error: 0, pending: 1, running: 2};
+ return states[a.name] - states[b.name];
+ });
+
+ // Append to status charts to non-subordinate services
+ var status_chart = node.append('g')
+ .attr('class', 'service-status')
+ .attr('transform', function(d) {
+ return 'translate(' + d.getRelativeCenter() + ')';
+ });
+
+ // Add a mask svg
+ status_chart.append('image')
+ .attr('xlink:href', '/juju-ui/assets/svgs/service_health_mask.svg')
+ .attr('width', function(d) {
+ return d.w / 3;
+ })
+ .attr('height', function(d) {
+ return d.h / 3;
+ })
+ .attr('x', function() {
+ return -d3.select(this).attr('width') / 2;
+ })
+ .attr('y', function() {
+ return -d3.select(this).attr('height') / 2;
+ });
+
+ // Add the path after the mask image (since it requires the mask's
+ // width to set its own).
+ var status_arcs = status_chart.selectAll('path')
+ .data(function(d) {
+ var aggregate_map = d.aggregated_status,
+ aggregate_list = [];
+ Y.Object.each(aggregate_map, function(count, state) {
+ aggregate_list.push({name: state, value: count});
+ });
+
+ return status_chart_layout(aggregate_list);
+ }).enter().insert('path', 'image')
+ .attr('d', status_chart_arc)
+ .attr('class', function(d) { return 'status-' + d.data.name; })
+ .attr('fill-rule', 'evenodd')
+ .append('title').text(function(d) {
+ return d.data.name;
+ });
+
+ // Add the unit counts, visible only on hover.
+ var unit_count = status_chart.append('text')
+ .attr('class', 'unit-count hide-count')
+ .text(function(d) {
+ return views.humanizeNumber(d.unit_count);
+ });
+ }
+
+
+
+ }, {
+ ATTRS: {}
+
+ });
+ views.ServiceModule = ServiceModule;
+}, '0.1.0', {
+ requires: [
+ 'd3',
+ 'd3-components',
+ 'node',
+ 'event',
+ 'juju-models',
+ 'juju-env'
+ ]
+});
=== added file 'app/views/topology/topology.js'
--- app/views/topology/topology.js 1970-01-01 00:00:00 +0000
+++ app/views/topology/topology.js 2012-11-21 19:26:21 +0000
@@ -0,0 +1,133 @@
+'use strict';
+
+YUI.add('juju-topology', function(Y) {
+ var views = Y.namespace('juju.views'),
+ models = Y.namespace('juju.models'),
+ d3ns = Y.namespace('d3');
+
+ /**
+ * Topology models and renders the SVG of the envionment topology
+ * with its associated behaviors.
+ *
+ * The line of where to put code (in the Topology vs a Module) isn't 100%
+ * clear. The rule of thumb to follow is that shared state, policy and
+ * configuration belong here. If the only shared requirement on shared state
+ * is watch/event like behavior fire an event and place the logic in a module.
+ *
+ * @class Topology
+ * @namespace juju.views
+ **/
+ var Topology = Y.Base.create('Topology', d3ns.Component, [], {
+ initializer: function(options) {
+ Topology.superclass.constructor.apply(this, arguments);
+ options = options || {};
+
+ },
+
+ render: function() {
+ Topology.superclass.render.apply(this, arguments);
+ return this;
+ },
+
+ renderOnce: function() {
+ var self = this,
+ vis,
+ width = this.get('width'),
+ height = this.get('height'),
+ container = this.get('container');
+
+ if (this.svg) {
+ return;
+ }
+ container.setHTML(views.Templates.overview());
+ // Take the first element.
+ this.svg = container.one(':first-child');
+
+ this.service_scale = d3.scale.log().range([150, 200]);
+ this.service_scale_width = d3.scale.log().range([164, 200]),
+ this.service_scale_height = d3.scale.log().range([64, 100]);
+ this.xscale = d3.scale.linear()
+ .domain([-width / 2, width / 2])
+ .range([0, width]),
+ this.yscale = d3.scale.linear()
+ .domain([-height / 2, height / 2])
+ .range([height, 0]);
+
+ // Default zoom behavior, customized in bindings.
+ this.zoom = d3.behavior.zoom();
+
+ // Set up the visualization with a pack layout.
+ vis = d3.select(container.getDOMNode())
+ .selectAll('.topology-canvas')
+ .append('svg:svg')
+ .attr('pointer-events', 'all')
+ .attr('width', width)
+ .attr('height', height)
+ .append('svg:g')
+ .append('g');
+
+ vis.append('svg:rect')
+ .attr('class', 'graph')
+ .attr('fill', 'rgba(255,255,255,0)');
+
+ this.vis = vis;
+
+ return this;
+ }
+
+ }, {
+ ATTRS: {
+ /**
+ * @property {models.Database} db
+ **/
+ db: {},
+ /**
+ * @property {store.Environment} env
+ **/
+ env: {},
+ /**
+ * @property {Array} size
+ * A [width, height] tuple representing canvas size.
+ **/
+ size: {value: [640, 480]},
+ /**
+ * @property {Number} scale
+ **/
+ scale: {
+ getter: function() {return this.zoom.scale();},
+ setter: function(v) {this.zoom.scale(v);}
+ },
+ /**
+ * @property {Array} transform
+ **/
+ transform: {
+ getter: function() {return this.get('zoom').transform();},
+ setter: function(v) {this.get('zoom').transform(v);}
+ },
+
+ width: {
+ getter: function() {return this.get('size')[0];}
+ },
+
+ height: {
+ getter: function() {return this.get('size')[1];}
+ }
+ }
+
+ });
+ views.Topology = Topology;
+}, '0.1.0', {
+ requires: [
+ 'd3',
+ 'd3-components',
+ 'node',
+ 'event',
+ 'juju-templates',
+ 'juju-models',
+ 'juju-env',
+ 'juju-topology-service',
+ 'juju-topology-relation',
+ 'juju-topology-panzoom',
+ 'juju-topology-viewport'
+ ]
+});
=== added file 'app/views/topology/viewport.js'
--- app/views/topology/viewport.js 1970-01-01 00:00:00 +0000
+++ app/views/topology/viewport.js 2012-11-21 19:26:21 +0000
@@ -0,0 +1,41 @@
+'use strict';
+
+YUI.add('juju-topology-viewport', function(Y) {
+ var views = Y.namespace('juju.views'),
+ models = Y.namespace('juju.models'),
+ d3ns = Y.namespace('d3');
+
+ /**
+ * @module topology-service
+ * @class Service
+ * @namespace juju.views
+ **/
+ var ViewportModule = Y.Base.create('ViewportModule', d3ns.Module, [], {
+ initializer: function(options) {
+ ViewportModule.superclass.constructor.apply(this, arguments);
+ },
+
+ render: function() {
+ ViewportModule.superclass.render.apply(this, arguments);
+ return this;
+ },
+
+ update: function() {
+ ViewportModule.superclass.update.apply(this, arguments);
+ return this;
+ }
+
+ }, {
+ ATTRS: {}
+ });
+ views.ViewportModule = ViewportModule;
+}, '0.1.0', {
+ requires: [
+ 'd3',
+ 'd3-components',
+ 'node',
+ 'event',
+ 'juju-models',
+ 'juju-env'
+ ]
+});
=== modified file 'test/index.html'
--- test/index.html 2012-11-20 15:35:30 +0000
+++ test/index.html 2012-11-21 19:26:21 +0000
@@ -16,6 +16,7 @@
</script>
<script src="test_d3_components.js"></script>
+ <script src="test_topology.js"></script>
<script src="test_env.js"></script>
<script src="test_model.js"></script>
<script src="test_notifications.js"></script>
=== modified file 'test/test_d3_components.js'
--- test/test_d3_components.js 2012-11-09 14:17:58 +0000
+++ test/test_d3_components.js 2012-11-21 19:26:21 +0000
@@ -30,6 +30,10 @@
cancelHandler: function(evt) {
state.cancelled = true;
+ },
+
+ componentBound: function() {
+ state.bound = true;
}
});
@@ -76,10 +80,6 @@
Y.fire('cancel');
state.cancelled.should.equal(true);
- // XXX: While on the plane I determined that things like
- // 'events' are sharing state with other runs/modules.
- // This must be fixed before this can work again.
-
// Manually set state, remove the module and test again
state.cancelled = false;
comp.removeModule('TestModule');
@@ -97,6 +97,10 @@
comp.render();
Y.one('.thing').simulate('click');
state.thing.should.equal('decorated');
+
+ // Also verify that the module's componentChanged binding
+ // took place.
+ state.bound.should.equal(true);
});
it('should allow event bindings through the use of a declartive object',
=== modified file 'test/test_environment_view.js'
--- test/test_environment_view.js 2012-11-20 16:22:21 +0000
+++ test/test_environment_view.js 2012-11-21 19:26:21 +0000
@@ -104,7 +104,7 @@
beforeEach(function(done) {
container = Y.Node.create('<div id="test-container" />');
- Y.one('body').prepend(container);
+ Y.one('body').append(container);
db = new models.Database();
db.on_delta({data: environment_delta});
done();
@@ -354,7 +354,7 @@
env: env
}).render();
var service = container.one('.service'),
- add_rel = container.one('.add-relation'),
+ add_rel = container.one('#service-menu .add-relation'),
after_evt;
// Mock endpoints
@@ -382,13 +382,7 @@
return endpoints;
};
- // Toggle the control panel for the Add Relation button.
- view.service_click_actions.toggleControlPanel(
- d3.select(service.getDOMNode()).datum(),
- view,
- service);
- // Mock an event object so that d3.mouse does not throw a NPE.
- d3.event = {};
+ service.simulate('click');
add_rel.simulate('click');
container.all('.selectable-service')
.size()
@@ -396,11 +390,7 @@
container.all('.dragline')
.size()
.should.equal(1);
- // Start the process of adding a relation.
- view.service_click_actions.ambiguousAddRelationCheck(
- d3.select(service.next().getDOMNode()).datum(),
- view,
- service.next());
+ service.next().simulate('click');
container.all('.selectable-service').size()
.should.equal(0);
// The database is initialized with three relations in beforeEach.
=== added file 'test/test_topology.js'
--- test/test_topology.js 1970-01-01 00:00:00 +0000
+++ test/test_topology.js 2012-11-21 19:26:21 +0000
@@ -0,0 +1,97 @@
+
+'use strict';
+
+describe.only('topology', function() {
+ var Y, NS, views,
+ TestModule, modA, state,
+ container, topo,
+ models,
+ db;
+
+ before(function(done) {
+ Y = YUI(GlobalConfig).use(['juju-topology',
+ 'd3-components',
+ 'node',
+ 'node-event-simulate'],
+ function(Y) {
+ NS = Y.namespace('d3');
+ views = Y.namespace('juju.views');
+ models = Y.namespace('juju.models');
+
+ TestModule = Y.Base.create('TestModule', NS.Module, [], {
+ events: {
+ scene: { '.thing': {click: 'decorateThing'}},
+ d3: {'.target': {click: 'targetTarget'}},
+ yui: {
+ cancel: 'cancelHandler'
+ }
+ },
+
+ decorateThing: function(evt) {
+ state.thing = 'decorated';
+ },
+
+ targetTarget: function(evt) {
+ state.targeted = true;
+ },
+
+ cancelHandler: function(evt) {
+ state.cancelled = true;
+ }
+ });
+
+ done();
+ });
+ });
+
+ beforeEach(function() {
+ container = Y.Node.create('<div id="test" style="visibility: hidden">' +
+ '<button class="thing"></button>' +
+ '<button class="target"></button>' +
+ '</div>');
+ state = {};
+ });
+
+ afterEach(function() {
+ container.remove();
+ container.destroy();
+ if (topo) {
+ topo.unbind();
+ }
+ if (db) {
+ db.destroy();
+ }
+ });
+
+ it('should be able to create a topology with default modules', function() {
+ topo = new views.Topology();
+ topo.setAttrs({container: container});
+ topo.addModule(TestModule);
+ topo.render();
+
+ // Verify that we have built the default scene.
+ Y.Lang.isValue(topo.svg).should.equal(true);
+ });
+
+ function createStandardTopo() {
+ db = new models.Database();
+ topo = new views.Topology();
+ topo.setAttrs({container: container, db: db});
+ topo.addModule(views.ServiceModule);
+ topo.addModule(views.RelationModule);
+ topo.addModule(views.PanZoomModule);
+ topo.addModule(views.ViewportModule);
+ return topo;
+ }
+
+ it('should be able to create a topology with standard env view modules',
+ function() {
+ topo = createStandardTopo();
+ topo.render();
+ // Verify that we have built the default scene.
+ Y.Lang.isValue(topo.svg).should.equal(true);
+ });
+
+});
+
+
Follow ups