← Back to team overview

yellow team mailing list archive

[Merge] lp:~bcsaller/juju-gui/viewport into lp:juju-gui

 

Benjamin Saller has proposed merging lp:~bcsaller/juju-gui/viewport into lp:juju-gui.

Requested reviews:
  Juju GUI Hackers (juju-gui)
Related bugs:
  Bug #1090479 in juju-gui: "Create viewport topology module"
  https://bugs.launchpad.net/juju-gui/+bug/1090479

For more details, see:
https://code.launchpad.net/~bcsaller/juju-gui/viewport/+merge/142138

Topology Viewport module

Enable viewport resizing via module.
-- 
https://code.launchpad.net/~bcsaller/juju-gui/viewport/+merge/142138
Your team Juju GUI Hackers is requested to review the proposed merge of lp:~bcsaller/juju-gui/viewport into lp:juju-gui.
=== modified file 'app/app.js'
--- app/app.js	2012-12-19 13:45:10 +0000
+++ app/app.js	2013-01-07 15:36:01 +0000
@@ -550,7 +550,7 @@
 
       this.showView('environment', options, {
         callback: function() {
-          this.views.environment.instance.postRender();
+          this.views.environment.instance.rendered();
         },
         render: true});
     },
@@ -745,18 +745,23 @@
 
 }, '0.5.2', {
   requires: [
+    'juju-charm-models',
+    'juju-charm-panel',
+    'juju-charm-store',
     'juju-models',
-    'juju-charm-models',
+    'juju-notifications',
+
+    // This alias doesn't seem to work, including refs by hand.
+    'juju-controllers',
+    'juju-notification-controller',
+    'juju-env',
+
     'juju-views',
-    'juju-controllers',
-    'juju-view-charm-search',
     'io',
     'json-parse',
     'app-base',
     'app-transitions',
     'base',
     'node',
-    'model',
-    'juju-charm-panel',
-    'juju-charm-store']
+    'model']
 });

=== modified file 'app/assets/javascripts/d3-components.js'
--- app/assets/javascripts/d3-components.js	2012-12-20 21:53:04 +0000
+++ app/assets/javascripts/d3-components.js	2013-01-07 15:36:01 +0000
@@ -194,18 +194,19 @@
 
       function _bindEvent(name, handler, container, selector, context) {
         // Adapt between d3 events and YUI delegates.
-        var d3Adaptor = function(evt) {
+        var d3Adapter = function(evt) {
           var selection = d3.select(evt.currentTarget.getDOMNode()),
               d = selection.data()[0];
           // This is a minor violation (extension)
           // of the interface, but suits us well.
           d3.event = evt;
+          console.debug('Handler for', name, selector);
           return handler.call(
               evt.currentTarget.getDOMNode(), d, context);
         };
 
         subscriptions.push(
-            Y.delegate(name, d3Adaptor, container, selector, context));
+            Y.delegate(name, d3Adapter, container, selector, context));
       }
 
       this.unbind(modName);
@@ -252,8 +253,7 @@
                 // (re)Register the event to bubble.
                 self.publish(name, {emitFacade: true});
               }
-              console.debug('d3 component yui event binding', target.toString(),
-                            eventPhase, name);
+              console.debug('yui event binding', module.name, eventPhase, name);
               subscriptions.push(
                   target[eventPhase](
                   name, callback, handler.context));
@@ -313,10 +313,11 @@
         Y.each(handlers, function(handler, trigger) {
           var adapter;
           handler = self._normalizeHandler(handler, module);
-          // Create an adaptor
+          // Create an adapter
           adapter = function() {
             var selection = d3.select(this),
                 d = selection.data()[0];
+            console.debug('D3 Handler for', selector, trigger);
             return handler.callback.call(this, d, handler.context);
           };
           d3.selectAll(selector).on(trigger, adapter);
@@ -325,7 +326,9 @@
     },
 
     /**
-     * Allow d3 event rebinding after rendering.
+     * Allow d3 event rebinding after rendering. The component
+     * can trigger this after its sure relevant elements
+     * are in the bound DOM.
      *
      **/
     bindAllD3Events: function() {

=== modified file 'app/modules-debug.js'
--- app/modules-debug.js	2012-12-21 12:52:30 +0000
+++ app/modules-debug.js	2013-01-07 15:36:01 +0000
@@ -12,9 +12,10 @@
   filter: 'debug',
   // Set "true" for verbose logging of YUI
   debug: false,
+
   base: '/juju-ui/assets/javascripts/yui/',
   // Use Rollups
-  combine: false,
+  combine: true,
 
   groups: {
     gallery: {
@@ -80,6 +81,7 @@
         'juju-topology': {
           fullpath: '/juju-ui/views/topology/topology.js'
         },
+
         'juju-view-utils': {
           fullpath: '/juju-ui/views/utils.js'
         },
@@ -158,7 +160,9 @@
         },
 
         'juju-controllers': {
-          use: ['juju-env', 'juju-charm-store',
+          use: [
+            'juju-env',
+            'juju-charm-store',
             'juju-notification-controller']
         },
 

=== modified file 'app/templates/overview.handlebars'
--- app/templates/overview.handlebars	2012-12-17 15:39:40 +0000
+++ app/templates/overview.handlebars	2013-01-07 15:36:01 +0000
@@ -1,5 +1,5 @@
 <div class="topology">
-    <div class="topology-canvas crosshatch-background">
+    <div class="crosshatch-background topology-canvas">
         <div class="environment-menu" id="service-menu">
             <div class="triangle">&nbsp;</div>
             <ul>

=== modified file 'app/views/charm-panel.js'
--- app/views/charm-panel.js	2012-12-14 20:25:16 +0000
+++ app/views/charm-panel.js	2013-01-07 15:36:01 +0000
@@ -1056,11 +1056,11 @@
     setPanel({name: 'charms'});
 
     // Update position if we resize the window.
-    Y.on('windowresize', function(e) {
+    subscriptions.push(Y.on('windowresize', function(e) {
       if (isPanelVisible) {
         updatePanelPosition();
       }
-    });
+    }));
 
     /**
      * Hide the charm panel.
@@ -1229,6 +1229,7 @@
   requires: [
     'view',
     'juju-view-utils',
+    'juju-templates',
     'node',
     'handlebars',
     'event-hover',

=== modified file 'app/views/environment.js'
--- app/views/environment.js	2012-12-20 17:28:17 +0000
+++ app/views/environment.js	2013-01-07 15:36:01 +0000
@@ -34,10 +34,10 @@
 
           //If we need the initial HTML template
           // take care of that.
-          if (!this.svg) {
+          if (!this._rendered) {
             EnvironmentView.superclass.render.apply(this, arguments);
             container.setHTML(Templates.overview());
-            this.svg = container.one('.topology');
+            this._rendered = true;
           }
 
           if (!topo) {
@@ -51,6 +51,7 @@
             // Bind all the behaviors we need as modules.
             topo.addModule(views.MegaModule);
             topo.addModule(views.PanZoomModule);
+            topo.addModule(views.ViewportModule);
             topo.addModule(views.RelationModule);
 
             topo.addTarget(this);
@@ -61,13 +62,15 @@
           return this;
         },
 
-        postRender: function() {
-          this.topo.attachContainer();
+        /**
+         * Render callback handler,
+         * triggered from app when the view renders.
+         *
+         * @method rendered
+         **/
+        rendered: function() {
           this.topo.fire('rendered');
-          // Bind d3 events (manually)
-          // this needs to be postRender and
-          // the jiggle in phases has broken
-          // the existing (from change to showView)
+          // Bind d3 events (manually).
           this.topo.bindAllD3Events();
         }
       }, {
@@ -79,13 +82,10 @@
   requires: ['juju-templates',
              'juju-view-utils',
              'juju-models',
-             'd3',
-             'd3-components',
+             'juju-topology',
+             'svg-layouts',
              'base-build',
              'handlebars-base',
              'node',
-             'svg-layouts',
-             'event-resize',
-             'slider',
              'view']
 });

=== modified file 'app/views/topology/mega.js'
--- app/views/topology/mega.js	2013-01-03 20:38:59 +0000
+++ app/views/topology/mega.js	2013-01-07 15:36:01 +0000
@@ -128,9 +128,6 @@
         }
       },
       yui: {
-        windowresize: {
-          callback: 'setSizesFromViewport',
-          context: 'module'},
         rendered: 'renderedHandler',
         show: 'show',
         hide: 'hide',
@@ -633,9 +630,6 @@
 
       this.update();
 
-      // Set the sizes from the viewport.
-      this.setSizesFromViewport();
-
       // Ensure relation labels are sized properly.
       container.all('.rel-label').each(function(label) {
         var width = label.one('text').getClientRect().width + 10;
@@ -681,57 +675,6 @@
       picker.one('.picker-expanded').removeClass('active');
     },
 
-    /*
-         * Set the visualization size based on the viewport
-         */
-    setSizesFromViewport: function() {
-      // This event allows other page components that may unintentionally
-      // affect the page size, such as the charm panel, to get out of the
-      // way before we compute sizes.  Note the
-      // "afterPageSizeRecalculation" event at the end of this function.
-      // start with some reasonable defaults
-      console.log('setSizesFromViewPort', this, arguments);
-      var topo = this.get('component'),
-          container = this.get('container'),
-          vis = topo.vis,
-          xscale = topo.xScale,
-          yscale = topo.yScale,
-          svg = container.one('svg'),
-          canvas = container.one('.topology-canvas');
-
-      topo.fire('beforePageSizeRecalculation');
-      // Get the canvas out of the way so we can calculate the size
-      // correctly (the canvas contains the svg).  We want it to be the
-      // smallest size we accept--no smaller or bigger--or else the
-      // presence or absence of scrollbars may affect our calculations
-      // incorrectly.
-      canvas.setStyles({height: 600, width: 800});
-      var dimensions = utils.getEffectiveViewportSize(true, 800, 600);
-      // Set the svg sizes.
-      svg.setAttribute('width', dimensions.width)
-            .setAttribute('height', dimensions.height);
-
-      // Set the internal rect's size.
-      svg.one('rect')
-            .setAttribute('width', dimensions.width)
-            .setAttribute('height', dimensions.height);
-      canvas
-            .setStyle('height', dimensions.height)
-            .setStyle('width', dimensions.width);
-
-      // Reset the scale parameters
-      topo.xScale.domain([-dimensions.width / 2, dimensions.width / 2])
-            .range([0, dimensions.width]);
-      topo.yScale.domain([-dimensions.height / 2, dimensions.height / 2])
-            .range([dimensions.height, 0]);
-
-      topo.set('size', [dimensions.width, dimensions.height]);
-      topo.fire('afterPageSizeRecalculation');
-    },
-
-    /*
-         * Update the location of the active service panel
-         */
     updateServiceMenuLocation: function() {
       var topo = this.get('component'),
           container = this.get('container'),
@@ -872,8 +815,6 @@
     'd3',
     'd3-components',
     'juju-templates',
-    'node',
-    'event',
     'juju-models',
     'juju-env'
   ]

=== modified file 'app/views/topology/panzoom.js'
--- app/views/topology/panzoom.js	2012-12-21 20:46:01 +0000
+++ app/views/topology/panzoom.js	2013-01-07 15:36:01 +0000
@@ -6,7 +6,7 @@
       d3ns = Y.namespace('d3');
 
   /**
-   * Handle PanZoom within the a Topology.
+   * Handle PanZoom within a Topology.
    *
    * Emitted events:
    *
@@ -36,20 +36,13 @@
       this._scale = 1.0;
     },
 
-    // Handler for 'zoom' event.
-    zoomHandler: function(evt) {
-      var s = this.slider,
-          vis = this.get('component').vis;
-
-      s.set('value', Math.floor(evt.scale * 100));
-      this.rescale(vis, evt);
-    },
-
     renderSlider: function() {
       var self = this,
           topo = this.get('component'),
+          options = topo.options,
+          currentScale = topo.get('scale'),
           value = 100,
-          currentScale = topo.get('scale');
+          slider;
 
       if (self.slider) {
         return;
@@ -58,30 +51,29 @@
       if (currentScale) {
         value = currentScale * 100;
       }
-      var slider = new Y.Slider({
-        min: 25,
-        max: 200,
+
+      slider = new Y.Slider({
+        min: options.minZoom,
+        max: options.maxZoom,
         value: value
       });
+      // XXX: selection to module option
       slider.render('#slider-parent');
       topo.recordSubscription(this,
                               slider.after('valueChange', function(evt) {
-                                // Don't fire a zoom if there's a zoom event
-                                // already in progress; that will run rescale
-                                // for us.
-                                if (d3.event && d3.event.scale &&
-                                    d3.event.translate) {
-                                  return;
-                                }
-                                self._fire_zoom((
-                                    evt.newVal - evt.prevVal) / 100);
+                                self._fire_zoom(
+                                (evt.newVal - evt.prevVal) / 100);
                               }));
       self.slider = slider;
     },
 
-    update: function() {
-      PanZoomModule.superclass.update.apply(this, arguments);
-      return this;
+    // Handler for 'zoom' event.
+    zoomHandler: function(evt) {
+      var slider = this.slider,
+          vis = this.get('component').vis;
+
+      slider.set('value', Math.floor(evt.scale * 100));
+      this.rescale(vis, evt);
     },
 
     /*
@@ -89,7 +81,7 @@
      */
     zoom_out: function(data, context) {
       var slider = context.slider,
-              val = slider.get('value');
+          val = slider.get('value');
       slider.set('value', val - 25);
     },
 
@@ -98,7 +90,7 @@
      */
     zoom_in: function(data, context) {
       var slider = context.slider,
-              val = slider.get('value');
+          val = slider.get('value');
       slider.set('value', val + 25);
     },
 
@@ -107,25 +99,30 @@
      */
     _fire_zoom: function(delta) {
       var topo = this.get('component'),
+          container = topo.get('container'),
+          dim = container.getClientRect(),
           vis = topo.vis,
           zoom = topo.zoom,
           evt = {};
 
+      if (!dim) {
+        return;
+      }
+
       // Build a temporary event that rescale can use of a similar
       // construction to d3.event.
       evt.translate = zoom.translate();
       evt.scale = zoom.scale() + delta;
-
       // Update the scale in our zoom behavior manager to maintain state.
       zoom.scale(evt.scale);
-
       // Update the translate so that we scale from the center
       // instead of the origin.
       var rect = vis.select('rect');
-      evt.translate[0] -= parseInt(rect.attr('width'), 10) / 2 * delta;
-      evt.translate[1] -= parseInt(rect.attr('height'), 10) / 2 * delta;
-      zoom.translate(evt.translate);
-
+      if (rect && rect.attr('width')) {
+        evt.translate[0] -= parseInt(rect.attr('width'), 10) / 2 * delta;
+        evt.translate[1] -= parseInt(rect.attr('height'), 10) / 2 * delta;
+        zoom.translate(evt.translate);
+      }
       this.rescale(vis, evt);
     },
 
@@ -180,10 +177,11 @@
   views.PanZoomModule = PanZoomModule;
 }, '0.1.0', {
   requires: [
+    'node',
+    'event',
+    'slider',
     'd3',
     'd3-components',
-    'node',
-    'event',
     'juju-models',
     'juju-env'
   ]

=== modified file 'app/views/topology/topology.js'
--- app/views/topology/topology.js	2012-12-21 18:48:19 +0000
+++ app/views/topology/topology.js	2013-01-07 15:36:01 +0000
@@ -25,7 +25,12 @@
   var Topology = Y.Base.create('Topology', d3ns.Component, [], {
     initializer: function(options) {
       Topology.superclass.constructor.apply(this, arguments);
-      this.options = Y.mix(options || {});
+      this.options = Y.mix(options || {
+        minZoom: 25,
+        maxZoom: 200
+      });
+
+      this._subscriptions = [];
     },
 
     /**
@@ -70,25 +75,9 @@
       // Take the first element.
       this._templateRendered = true;
 
-      // Create a pan/zoom behavior manager.
-      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]);
-
-      // Include very basic behavior, fire
-      // yui event for anything more complex.
-      this.zoom = d3.behavior.zoom()
-                    .x(this.xScale)
-                    .y(this.yScale)
-                    .scaleExtent([0.25, 2.0])
-                    .on('zoom', function(evt) {
-                        // This will add the d3 properties to the
-                        // eventFacade
-                        self.fire('zoom', d3.event);
-                     });
+      // These are defaults, a (Viewport) Module
+      // can implement policy around them.
+      this.computeScales();
 
       // Set up the visualization with a pack layout.
       vis = d3.select(container.getDOMNode())
@@ -98,7 +87,8 @@
               .attr('width', width)
               .attr('height', height)
               .append('svg:g')
-              .call(this.zoom)
+              .attr('class', 'zoom-plane')
+              .call(this.zoom) // Set by computeScales.
               .append('g');
 
       vis.append('svg:rect')
@@ -107,39 +97,46 @@
 
       this.vis = vis;
 
-      // Build out scale and zoom.
-      // These are defaults, a (Viewport) Module
-      // can implement policy around them.
-      this.sizeChangeHandler();
-      this.on('sizeChanged', this.sizeChangeHandler);
-
       Topology.superclass.renderOnce.apply(this, arguments);
       return this;
     },
 
-    sizeChangeHandler: function() {
+    computeScales: function() {
       var self = this,
           width = this.get('width'),
           height = this.get('height');
 
+      if (!this.xScale) {
+        this.xScale = d3.scale.linear();
+        this.yScale = d3.scale.linear();
+        this.zoom = d3.behavior.zoom();
+      }
       // Update the pan/zoom behavior manager.
       this.xScale.domain([-width / 2, width / 2])
-        .range([0, width]);
+        .range([0, width])
+        .clamp(true)
+        .nice();
       this.yScale.domain([-height / 2, height / 2])
-        .range([height, 0]);
+        .range([height, 0])
+        .clamp(true)
+        .nice();
+
       this.zoom.x(this.xScale)
-        .y(this.yScale);
+               .y(this.yScale)
+               .scaleExtent([this.options.minZoom, this.options.maxZoom])
+               .on('zoom', function(evt) {self.fire('zoom', d3.event);});
+      // After updating scale allow modules to perform any needed updates.
+      this.fire('rescaled');
     },
 
     /*
-         * Utility method to get a service object from the DB
-         * given a BoundingBox.
-         */
+     * 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);
     }
-
   }, {
     ATTRS: {
       /**

=== modified file 'app/views/topology/viewport.js'
--- app/views/topology/viewport.js	2012-12-11 03:58:03 +0000
+++ app/views/topology/viewport.js	2013-01-07 15:36:01 +0000
@@ -2,6 +2,7 @@
 
 YUI.add('juju-topology-viewport', function(Y) {
   var views = Y.namespace('juju.views'),
+      utils = Y.namespace('juju.views.utils'),
       models = Y.namespace('juju.models'),
       d3ns = Y.namespace('d3');
 
@@ -26,112 +27,53 @@
 
     events: {
       yui: {
-        windowresize: 'resized'
-      }
-    },
-
-    initializer: function(options) {
-      ViewportModule.superclass.constructor.apply(this, arguments);
-    },
-
-    render: function() {
-      var topology = this.get('component'),
-          value = 100,
-          currentScale = topology.get('scale');
-
-      ViewportModule.superclass.render.apply(this, arguments);
-      // Build a slider to control zoom level
-      if (currentScale) {
-        value = currentScale * 100;
-      }
-      var slider = new Y.Slider({
-        min: 25,
-        max: 200,
-        value: value
-      });
-      slider.render('#slider-parent');
-      slider.after('valueChange', function(evt) {
-        // Don't fire a zoom if there's a zoom event already in progress;
-        // that will run rescale for us.
-        if (d3.event && d3.event.scale && d3.event.translate) {
-          return;
-        }
-        topology._fire_zoom((evt.newVal - evt.prevVal) / 100);
-      });
-      this.slider = slider;
-
-      return this;
-    },
-
-    update: function() {
-      ViewportModule.superclass.update.apply(this, arguments);
-      return this;
-    },
-
-    /**
-     * Event handler for windowresize events.
-     *
-     * Properly scale the component to take advantage of all the space
-     * provided by the viewport.
-     *
-     * @method resized
-     **/
-    resized: function(evt) {
+        windowresize: 'resized',
+        rendered: 'resized'
+      }
+    },
+
+    /*
+     * Set the visualization size based on the viewport
+     */
+    resized: function() {
+      // This event allows other page components that may unintentionally
+      // affect the page size, such as the charm panel, to get out of the
+      // way before we compute sizes.  Note the
+      // "afterPageSizeRecalculation" event at the end of this function.
       // start with some reasonable defaults
-      var topology = this.get('component'),
-          vis = topology.vis,
+      var topo = this.get('component'),
           container = this.get('container'),
-          viewport_height = '100%',
-          viewport_width = '100%',
+          vis = topo.vis,
           svg = container.one('svg'),
-          width = 800,
-          height = 600;
-
-      if (container.get('winHeight') &&
-          Y.one('#overview-tasks') &&
-          Y.one('.navbar')) {
-        // Attempt to get the viewport height minus the navbar at top and
-        // control bar at the bottom. Use Y.one() to ensure that the
-        // container is attached first (provides some sensible defaults)
-
-        viewport_height = container.get('winHeight') -
-            styleToNumber('#overview-tasks', 'height', 22) - //XXX
-            styleToNumber('.navbar', 'height', 87) - 1; //XXX
-
-        // Attempt to get the viewport width from the overview-tasks bar.
-        viewport_width = styleToNumber('#viewport', 'width', 800); //XXX
-
-        // Make sure we don't get sized any smaller than 800x600
-        viewport_height = Math.max(viewport_height, height);
-        viewport_width = Math.max(viewport_width, width);
+          canvas = container.one('.topology-canvas'),
+          rect = container.one('rect'),
+          newSize = {};
+
+      if (!canvas || !svg) {
+        return;
       }
-      // Set the svg sizes.
-      svg.setAttribute('width', viewport_width)
-        .setAttribute('height', viewport_height);
-
-      // Get the resulting computed sizes (in the case of 100%).
-      width = parseInt(svg.getComputedStyle('width'), 10);
-      height = parseInt(svg.getComputedStyle('height'), 10);
-
-      // Set the internal rect's size.
-      svg.one('rect')
-        .setAttribute('width', width)
-        .setAttribute('height', height);
-      container.one('#canvas').setStyle('height', height);
-      container.one('#canvas').setStyle('width', width);
-
+      topo.fire('beforePageSizeRecalculation');
+      // Get the canvas out of the way so we can calculate the size
+      // correctly (the canvas contains the svg).  We want it to be the
+      // smallest size we accept--no smaller or bigger--or else the
+      // presence or absence of scrollbars may affect our calculations
+      // incorrectly.
+      canvas.setStyles({height: 600, width: 800});
+      var dimensions = utils.getEffectiveViewportSize(true, 800, 600);
+      svg.setAttribute('width', dimensions.width);
+      svg.setAttribute('height', dimensions.height);
+
+      newSize.width = dimensions.width;
+      newSize.height = dimensions.height;
+      svg.one('.zoom-plane').setStyles(newSize);
+      canvas.setStyles(newSize);
+      rect.setAttribute('width', dimensions.width);
+      rect.setAttribute('height', dimensions.height);
       // Reset the scale parameters
-      topology.xscale.domain([-width / 2, width / 2])
-        .range([0, width]);
-      topology.yscale.domain([-height / 2, height / 2])
-        .range([height, 0]);
-
-      topology.width = width;
-      topology.height = height;
+      topo.set('size', [dimensions.width, dimensions.height]);
+      topo.fire('afterPageSizeRecalculation');
     }
 
-
-
   }, {
     ATTRS: {}
   });

=== modified file 'app/views/utils.js'
--- app/views/utils.js	2012-12-20 21:59:21 +0000
+++ app/views/utils.js	2013-01-07 15:36:01 +0000
@@ -118,6 +118,8 @@
           time: noop,
           timeEnd: noop,
           log: noop,
+          info: noop,
+          error: noop,
           debug: noop
         };
 
@@ -971,11 +973,11 @@
 
 }, '0.1.0', {
   requires: ['base-build',
-    'handlebars',
-    'node',
-    'view',
-    'panel',
-    'json-stringify',
-    'gallery-markdown',
-    'datatype-date-format']
+             'handlebars',
+             'node',
+             'view',
+             'panel',
+             'json-stringify',
+             'gallery-markdown',
+             'datatype-date-format']
 });

=== modified file 'lib/views/stylesheet.less'
--- lib/views/stylesheet.less	2012-12-05 15:12:45 +0000
+++ lib/views/stylesheet.less	2013-01-07 15:36:01 +0000
@@ -222,7 +222,6 @@
     position: relative;
 }
 
-
 .environment-menu {
     @border_radius: 20px;
     @background_color: #282421;

=== modified file 'package.json'
--- package.json	2013-01-02 12:57:09 +0000
+++ package.json	2013-01-07 15:36:01 +0000
@@ -15,8 +15,9 @@
     "cryptojs": ">= 2.5.3"
   },
   "devDependencies": {
-    "d3": "2.10.x",
+    "d3": "<3.0.0",
     "yui": ">=3.7.0",
+    "yeti": ">=0.2.0",
     "mocha": "1.5.x",
     "express": "3.x",
     "expect.js": "0.1.2",

=== modified file 'test/index.html'
--- test/index.html	2012-12-21 12:06:26 +0000
+++ test/index.html	2013-01-07 15:36:01 +0000
@@ -3,64 +3,80 @@
 <head>
   <meta charset="utf-8">
   <link rel="stylesheet" href="assets/mocha.css">
+
+
+  <!-- Load test runner/environment -->
+  <script src="assets/chai.js"></script>
+  <script src="assets/mocha.js"></script>
+  <script>
+    var assert = chai.assert,
+        expect = chai.expect;
+
+    var should = chai.should();
+    console.log('mocha setup');
+    mocha.setup({'ui': 'bdd', 'ignoreLeaks': false, 'timeout': 20000})
+    console.log('mocha setup done');
+  </script>
+
+  <!-- Load up YUI base, app modules, and test utils -->
+  <!-- Since only the tests depend on these files and the prod tests disable
+       the YUI loader, we have to include them manually here. -->
   <script src="/juju-ui/assets/modules.js"></script>
   <script src="/juju-ui/assets/all-yui.js"></script>
-  <!-- Since only the tests depend on these files and the prod tests disable
-    the YUI loader, we have to include them manually here. -->
   <script src="/juju-ui/assets/event-simulate.js"></script>
   <script src="/juju-ui/assets/node-event-simulate.js"></script>
-  <script src="assets/chai.js"></script>
-  <script src="assets/mocha.js"></script>
   <script src="utils.js"></script>
-  <script>
-    var assert = chai.assert,
-        expect = chai.expect
-        should = chai.should();
-    mocha.setup({'ui': 'bdd', 'ignoreLeaks': false})
-  </script>
-
+
+
+  <!-- Tests (Alphabetical)-->
+  <script src="test_app.js"></script>
+  <script src="test_app_hotkeys.js"></script>
+  <script src="test_application_notifications.js"></script>
+  <script src="test_charm_collection_view.js"></script>
+  <script src="test_charm_configuration.js"></script>
+  <script src="test_charm_panel.js"></script>
+  <script src="test_charm_store.js"></script>
+  <script src="test_charm_view.js"></script>
+  <script src="test_console.js"></script> 
   <script src="test_d3_components.js"></script>
-  <script src="test_topology.js"></script>
-  <script src="test_panzoom.js"></script>
+  <script src="test_environment_view.js"></script>
   <script src="test_env.js"></script>
+  <script src="test_endpoints.js"></script>
   <script src="test_model.js"></script>
   <script src="test_notifications.js"></script>
-  <script src="test_app.js"></script>
-  <script src="test_unit_view.js"></script>
-  <script src="test_charm_collection_view.js"></script>
-  <script src="test_charm_view.js"></script>
-  <script src="test_environment_view.js"></script>
+  <script src="test_notifier_widget.js"></script>
+  <script src="test_panzoom.js"></script>
+  <script src="test_topology.js"></script>
   <script src="test_service_config_view.js"></script>
   <script src="test_service_view.js"></script>
+  <script src="test_unit_view.js"></script>
   <script src="test_utils.js"></script>
-  <script src="test_charm_panel.js"></script>
-  <script src="test_charm_configuration.js"></script>
-  <script src="test_console.js"></script>
-  <script src="test_endpoints.js"></script>
-  <script src="test_application_notifications.js"></script>
-  <script src="test_charm_store.js"></script>
-  <script src="test_app_hotkeys.js"></script>
-  <script src="test_notifier_widget.js"></script>
+
 
   <script>
-  YUI().use('node', 'event', function(Y) {
-     Y.on('domready', function() {
-
-     var config = GlobalConfig;
-     for (group in config.groups) {
+  YUI_config = {
+      async: false,
+      consoleEnabled: true,
+      delayUntil: 'domready'
+  };
+
+  YUI().use(['node', 'event'], function(Y) {
+      var config = GlobalConfig;
+
+      for (group in config.groups) {
           var group = config.groups[group];
-         for (m in group.modules) {
-            var resource = group.modules[m];
-            if (!m || !resource.fullpath) {
-              continue
-            }
-            resource.fullpath = resource.fullpath.replace(
-              '/juju-ui/', '../juju-ui/', 1);
-         }
-     }
-     // Load before test runner
-     mocha.run();
-     });
+          for (m in group.modules) {
+              var resource = group.modules[m];
+              if (!m || !resource.fullpath) {
+                  continue
+              }
+              resource.fullpath = resource.fullpath.replace(
+                  '/juju-ui/', '../juju-ui/');
+          }
+      }
+      // Run the tests.
+      if (window.mochaPhantomJS) { mochaPhantomJS.run(); }
+      else { mocha.run(); }
   });
   </script>
 

=== modified file 'test/test_app.js'
--- test/test_app.js	2012-12-03 20:24:44 +0000
+++ test/test_app.js	2013-01-07 15:36:01 +0000
@@ -31,9 +31,18 @@
   return app;
 }
 
-YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils'], function(Y) {
+(function() {
+
   describe('Application basics', function() {
-    var app, container;
+    var Y, app, container;
+
+    before(function(done) {
+      Y = YUI(GlobalConfig).use(
+          ['juju-gui', 'juju-tests-utils'],
+          function(Y) {
+            done();
+          });
+    });
 
     beforeEach(function() {
       container = Y.one('#main')
@@ -116,14 +125,22 @@
     });
 
   });
-});
-
-YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils'], function(Y) {
+})();
+
+
+
+(function() {
+
   describe('Application Connection State', function() {
-    var container;
+    var container, Y;
 
-    before(function() {
-      container = Y.Node.create('<div id="test" class="container"></div>');
+    before(function(done) {
+      Y = YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils'],
+          function(Y) {
+            container = Y.Node.create(
+                '<div id="test" class="container"></div>');
+            done();
+          });
     });
 
     it('should be able to handle env connection status changes', function() {
@@ -134,7 +151,6 @@
           reset_called = false,
           noop = function() {return this;};
 
-
       // mock the db
       app.db = {
         // mock out notifications
@@ -162,15 +178,23 @@
     });
 
   });
-});
-
-YUI(GlobalConfig).use(['juju-models', 'juju-gui', 'datasource-local',
-  'juju-tests-utils', 'json-stringify'], function(Y) {
+})();
+
+
+(function() {
+
   describe('Application prefetching', function() {
-    var models, conn, env, app, container, charm_store, data, juju;
+    var Y, models, conn, env, app, container, charm_store, data, juju;
 
-    before(function() {
-      models = Y.namespace('juju.models');
+    before(function(done) {
+      console.log('Loading App prefetch test code');
+      Y = YUI(GlobalConfig).use(
+          ['juju-gui', 'datasource-local',
+           'juju-views', 'juju-templates',
+           'juju-tests-utils', 'json-stringify'], function(Y) {
+            models = Y.namespace('juju.models');
+            done();
+          });
     });
 
     beforeEach(function() {
@@ -237,4 +261,4 @@
       get_endpoints_count.should.equal(2);
     });
   });
-});
+})();

=== modified file 'test/test_app_hotkeys.js'
--- test/test_app_hotkeys.js	2012-11-27 14:25:32 +0000
+++ test/test_app_hotkeys.js	2013-01-07 15:36:01 +0000
@@ -1,11 +1,11 @@
 'use strict';
 
-YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils', 'node-event-simulate'],
-    function(Y) {
-      describe('application hotkeys', function() {
-        var app, container, windowNode;
+describe('application hotkeys', function() {
+  var app, container, windowNode, Y;
 
-        before(function(done) {
+  before(function(done) {
+    Y = YUI(GlobalConfig).use(
+        ['juju-gui', 'juju-tests-utils', 'node-event-simulate'], function(Y) {
           var env = {
             after: function() {},
             get: function() {},
@@ -21,43 +21,45 @@
           done();
         });
 
-        beforeEach(function() {
-          container = Y.Node.create('<div/>');
-          Y.one('#main').append(container);
-          app.render();
-        });
-
-        afterEach(function() {
-          container.remove(true);
-        });
-
-        it('should listen for alt-S events', function() {
-          var searchInput = Y.Node.create('<input/>');
-          searchInput.set('id', 'charm-search-field');
-          container.append(searchInput);
-          windowNode.simulate('keydown', {
-            keyCode: 83, // "S" key.
-            altKey: true
-          });
-          // Did charm-search-field get the focus?
-          assert.equal(searchInput, Y.one(document.activeElement));
-        });
-
-        it('should listen for alt-E events', function() {
-          var altEtriggered = false;
-          app.on('navigateTo', function(ev) {
-            if (ev && ev.url === '/') {
-              altEtriggered = true;
-            }
-            // Avoid URL change performed by additional listeners.
-            ev.stopImmediatePropagation();
-          });
-          windowNode.simulate('keydown', {
-            keyCode: 69, // "E" key.
-            altKey: true
-          });
-          assert.isTrue(altEtriggered);
-        });
-
-      });
-    });
+  });
+
+  beforeEach(function() {
+    container = Y.Node.create('<div/>');
+    Y.one('#main').append(container);
+    app.render();
+  });
+
+  afterEach(function() {
+    container.remove(true);
+  });
+
+  it('should listen for alt-S events', function() {
+    var searchInput = Y.Node.create('<input/>');
+    searchInput.set('id', 'charm-search-field');
+    container.append(searchInput);
+    windowNode.simulate('keydown', {
+      keyCode: 83, // "S" key.
+      altKey: true
+    });
+    // Did charm-search-field get the focus?
+    assert.equal(searchInput, Y.one(document.activeElement));
+  });
+
+  it('should listen for alt-E events', function() {
+    var altEtriggered = false;
+    app.on('navigateTo', function(ev) {
+      if (ev && ev.url === '/') {
+        altEtriggered = true;
+      }
+      // Avoid URL change performed by additional listeners.
+      ev.stopImmediatePropagation();
+    });
+    windowNode.simulate('keydown', {
+      keyCode: 69, // "E" key.
+      altKey: true
+    });
+    assert.isTrue(altEtriggered);
+  });
+
+});
+

=== modified file 'test/test_d3_components.js'
--- test/test_d3_components.js	2012-12-19 13:45:10 +0000
+++ test/test_d3_components.js	2013-01-07 15:36:01 +0000
@@ -32,7 +32,6 @@
           state.cancelled = true;
         }
       });
-
       done();
     });
   });

=== modified file 'test/test_environment_view.js'
--- test/test_environment_view.js	2012-12-21 18:48:19 +0000
+++ test/test_environment_view.js	2013-01-07 15:36:01 +0000
@@ -257,7 +257,7 @@
       }).render();
       // Attach the view to the DOM so that sizes get set properly
       // from the viewport (only available from DOM).
-      view.postRender();
+      view.rendered();
       var zoom_in = container.one('#zoom-in-btn'),
           zoom_out = container.one('#zoom-out-btn'),
           module = view.topo.modules.PanZoomModule,
@@ -293,7 +293,7 @@
          }).render();
          // Attach the view to the DOM so that sizes get set properly
          // from the viewport (only available from DOM).
-         view.postRender();
+         view.rendered();
          var svg = Y.one('svg');
 
          parseInt(svg.one('rect').getAttribute('height'), 10)
@@ -323,7 +323,7 @@
          }).render();
          // Attach the view to the DOM so that sizes get set properly
          // from the viewport (only available from DOM).
-         view.postRender();
+         view.rendered();
          var svg = container.one('svg'),
              canvas = container.one('.topology');
          // We have to hide the canvas so it does not affect our calculations.
@@ -504,7 +504,7 @@
             db: db,
             env: env
           }).render();
-          view.postRender();
+          view.rendered();
           var picker = container.one('.graph-list-picker'),
               button = picker.one('.picker-button');
           button.after('click', function() {

=== modified file 'test/test_notifications.js'
--- test/test_notifications.js	2012-11-23 16:21:32 +0000
+++ test/test_notifications.js	2013-01-07 15:36:01 +0000
@@ -1,462 +1,477 @@
 'use strict';
 
-YUI(GlobalConfig).use(['juju-gui', 'node-event-simulate', 'juju-tests-utils'],
+describe('notifications', function() {
+  var Y, juju, models, views;
+
+  var default_env = {
+    'result': [
+      ['service', 'add', {
+        'charm': 'cs:precise/wordpress-6',
+        'id': 'wordpress',
+        'exposed': false
+      }],
+      ['service', 'add', {
+        'charm': 'cs:precise/mediawiki-3',
+        'id': 'mediawiki',
+        'exposed': false
+      }],
+      ['service', 'add', {
+        'charm': 'cs:precise/mysql-6',
+        'id': 'mysql'
+      }],
+      ['relation', 'add', {
+        'interface': 'reversenginx',
+        'scope': 'global',
+        'endpoints':
+         [['wordpress', {'role': 'peer', 'name': 'loadbalancer'}]],
+        'id': 'relation-0000000000'
+      }],
+      ['relation', 'add', {
+        'interface': 'mysql',
+        'scope': 'global',
+        'endpoints':
+         [['mysql', {'role': 'server', 'name': 'db'}],
+          ['wordpress', {'role': 'client', 'name': 'db'}]],
+        'id': 'relation-0000000001'
+      }],
+      ['machine', 'add', {
+        'agent-state': 'running',
+        'instance-state': 'running',
+        'id': 0,
+        'instance-id': 'local',
+        'dns-name': 'localhost'
+      }],
+      ['unit', 'add', {
+        'machine': 0,
+        'agent-state': 'started',
+        'public-address': '192.168.122.113',
+        'id': 'wordpress/0'
+      }],
+      ['unit', 'add', {
+        'machine': 0,
+        'agent-state': 'error',
+        'public-address': '192.168.122.222',
+        'id': 'mysql/0'
+      }]
+    ],
+    'op': 'delta'
+  };
+
+
+  before(function(done) {
+    Y = YUI(GlobalConfig).use([
+      'juju-models',
+      'juju-views',
+      'juju-gui',
+      'juju-env',
+      'node-event-simulate',
+      'juju-tests-utils'],
+
     function(Y) {
-      describe('notifications', function() {
-        var juju, models, views;
-
-        var default_env = {
-          'result': [
-            ['service', 'add', {
-              'charm': 'cs:precise/wordpress-6',
-              'id': 'wordpress',
-              'exposed': false
-            }],
-            ['service', 'add', {
-              'charm': 'cs:precise/mediawiki-3',
-              'id': 'mediawiki',
-              'exposed': false
-            }],
-            ['service', 'add', {
-              'charm': 'cs:precise/mysql-6',
-              'id': 'mysql'
-            }],
-            ['relation', 'add', {
-              'interface': 'reversenginx',
-              'scope': 'global',
-              'endpoints':
-               [['wordpress', {'role': 'peer', 'name': 'loadbalancer'}]],
-              'id': 'relation-0000000000'
-            }],
-            ['relation', 'add', {
-              'interface': 'mysql',
-              'scope': 'global',
-              'endpoints':
-               [['mysql', {'role': 'server', 'name': 'db'}],
-                ['wordpress', {'role': 'client', 'name': 'db'}]],
-              'id': 'relation-0000000001'
-            }],
-            ['machine', 'add', {
-              'agent-state': 'running',
-              'instance-state': 'running',
-              'id': 0,
-              'instance-id': 'local',
-              'dns-name': 'localhost'
-            }],
-            ['unit', 'add', {
-              'machine': 0,
-              'agent-state': 'started',
-              'public-address': '192.168.122.113',
-              'id': 'wordpress/0'
-            }],
-            ['unit', 'add', {
-              'machine': 0,
-              'agent-state': 'error',
-              'public-address': '192.168.122.222',
-              'id': 'mysql/0'
-            }]
-          ],
-          'op': 'delta'
-        };
-
-
-        before(function() {
-          juju = Y.namespace('juju');
-          models = Y.namespace('juju.models');
-          views = Y.namespace('juju.views');
-        });
-
-        it('must be able to make notification and lists of notifications',
-           function() {
-             var note1 = new models.Notification({
-               title: 'test1',
-               message: 'Hello'
-             }),
-             note2 = new models.Notification({
-               title: 'test2',
-               message: 'I said goodnight!'
-             }),
-             notifications = new models.NotificationList();
-
-             notifications.add([note1, note2]);
-             notifications.size().should.equal(2);
-
-             // timestamp should be generated once
-             var ts = note1.get('timestamp');
-             note1.get('timestamp').should.equal(ts);
-             // force an update so we can test ordering
-             // fast execution can result in same timestamp
-             note2.set('timestamp', ts + 1);
-             note2.get('timestamp').should.be.above(ts);
-
-             // defaults as expected
-             note1.get('level').should.equal('info');
-             note2.get('level').should.equal('info');
-             // the sort order on the list should be by
-             // timestamp
-             notifications.get('title').should.eql(['test2', 'test1']);
-           });
-
-        it('must be able to render its view with sample data',
-           function() {
-             var note1 = new models.Notification({
-               title: 'test1', message: 'Hello'}),
-             note2 = new models.Notification({
-               title: 'test2', message: 'I said goodnight!'}),
-             notifications = new models.NotificationList(),
-             container = Y.Node.create('<div id="test">'),
-             env = new juju.Environment(),
-             view = new views.NotificationsView({
-                     container: container,
-                     notifications: notifications,
-                     env: env});
-             view.render();
-             // Verify the expected elements appear in the view
-             container.one('#notify-list').should.not.equal(undefined);
-             container.destroy();
-           });
-
-        it('must be able to limit the size of notification events',
-           function() {
-             var note1 = new models.Notification({
-               title: 'test1',
-               message: 'Hello'
-             }),
-             note2 = new models.Notification({
-               title: 'test2',
-               message: 'I said goodnight!'
-             }),
-             note3 = new models.Notification({
-               title: 'test3',
-               message: 'Never remember'
-             }),
-             notifications = new models.NotificationList({
-               max_size: 2
-             });
-
-             notifications.add([note1, note2]);
-             notifications.size().should.equal(2);
-
-             // Adding a new notification should pop the oldest from the list
-             // (we exceed max_size)
-             notifications.add(note3);
-             notifications.size().should.equal(2);
-             notifications.get('title').should.eql(['test3', 'test2']);
-           });
-
-        it('must be able to get notifications for a given model',
-           function() {
-             var m = new models.Service({id: 'mediawiki'}),
-             note1 = new models.Notification({
-               title: 'test1',
-               message: 'Hello',
-               modelId: m
-             }),
-             note2 = new models.Notification({
-               title: 'test2',
-               message: 'I said goodnight!'
-             }),
-             notifications = new models.NotificationList();
-
-             notifications.add([note1, note2]);
-             notifications.size().should.equal(2);
-             notifications.getNotificationsForModel(m).should.eql(
-             [note1]);
-
-           });
-
-        it('must be able to include and show object links', function() {
-          var container = Y.Node.create('<div id="test">'),
-              conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
-              env = new juju.Environment({conn: conn}),
-              app = new Y.juju.App({env: env, container: container}),
-              db = app.db,
-              mw = db.services.create({id: 'mediawiki',
-                                      name: 'mediawiki'}),
-              notifications = db.notifications,
-              view = new views.NotificationsOverview({
-                        container: container,
-                        notifications: notifications,
-                        app: app,
-                        env: env}).render();
-          // we use overview here for testing as it defaults
-          // to showing all notices
-
-          // we can use app's routing table to derive a link
-          notifications.create({title: 'Service Down',
-            message: 'Your service has an error',
-            link: app.getModelURL(mw)
-          });
-          view.render();
-          var link = container.one('.notice').one('a');
-          link.getAttribute('href').should.equal(
-              '/service/mediawiki/');
-          link.getHTML().should.contain('View Details');
-
-
-          // create a new notice passing the link_title
-          notifications.create({title: 'Service Down',
-            message: 'Your service has an error',
-            link: app.getModelURL(mw),
-            link_title: 'Resolve this'
-          });
-          view.render();
-          link = container.one('.notice').one('a');
-          link.getAttribute('href').should.equal(
-              '/service/mediawiki/');
-          link.getHTML().should.contain('Resolve this');
-        });
-
-        it('must be able to evict irrelevant notices', function() {
-          var container = Y.Node.create(
-              '<div id="test" class="container"></div>'),
-              conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
-              env = new juju.Environment({conn: conn}),
-              app = new Y.juju.App({
-                env: env,
-                container: container,
-                viewContainer: container
-              });
-          var environment_delta = default_env;
-
-          var notifications = app.db.notifications,
-              view = new views.NotificationsView({
-                container: container,
-                notifications: notifications,
-                env: app.env}).render();
-
-
-          app.env.dispatch_result(environment_delta);
-
-
-          notifications.size().should.equal(7);
-          // we have one unit in error
-          view.getShowable().length.should.equal(1);
-
-          // now fire another delta event marking that node as
-          // started
-          app.env.dispatch_result({result: [['unit', 'change', {
-            'machine': 0,
-            'agent-state': 'started',
-            'public-address': '192.168.122.222',
-            'id': 'mysql/0'
-          }]], op: 'delta'});
-          notifications.size().should.equal(8);
-          // This should have evicted the prior notice from seen
-          view.getShowable().length.should.equal(0);
-        });
-
-        it('must properly construct title and message based on level from ' +
-           'event data',
-           function() {
-             var container = Y.Node.create(
-             '<div id="test" class="container"></div>'),
-             app = new Y.juju.App({
-               container: container,
-               viewContainer: container
-             });
-             var environment_delta = {
-               'result': [
-                 ['service', 'add', {
-                   'charm': 'cs:precise/wordpress-6',
-                   'id': 'wordpress'
-                 }],
-                 ['service', 'add', {
-                   'charm': 'cs:precise/mediawiki-3',
-                   'id': 'mediawiki'
-                 }],
-                 ['service', 'add', {
-                   'charm': 'cs:precise/mysql-6',
-                   'id': 'mysql'
-                 }],
-                 ['unit', 'add', {
-                   'agent-state': 'install-error',
-                   'id': 'wordpress/0'
-                 }],
-                 ['unit', 'add', {
-                   'agent-state': 'error',
-                   'public-address': '192.168.122.222',
-                   'id': 'mysql/0'
-                 }],
-                 ['unit', 'add', {
-                   'public-address': '192.168.122.222',
-                   'id': 'mysql/2'
-                 }]
-               ],
-               'op': 'delta'
-             };
-
-             var notifications = app.db.notifications,
-             view = new views.NotificationsView({
-               container: container,
-               notifications: notifications,
-               app: app,
-               env: app.env}).render();
-
-             app.env.dispatch_result(environment_delta);
-
-             notifications.size().should.equal(6);
-             // we have one unit in error
-             var showable = view.getShowable();
-             showable.length.should.equal(2);
-             // The first showable notification should indicate an error.
-             showable[0].level.should.equal('error');
-             showable[0].title.should.equal('Error with mysql/0');
-             showable[0].message.should.equal('Agent-state = error.');
-             // The second showable notification should also indicate an error.
-             showable[1].level.should.equal('error');
-             showable[1].title.should.equal('Error with wordpress/0');
-             showable[1].message.should.equal('Agent-state = install-error.');
-             // The first non-error notice should have an 'info' level and less
-             // severe messaging.
-             var notice = notifications.item(0);
-             notice.get('level').should.equal('info');
-             notice.get('title').should.equal('Problem with mysql/2');
-             notice.get('message').should.equal('');
-           });
-
-
-        it('should open on click and close on clickoutside', function(done) {
-          var container = Y.Node.create('<div id="test-container" ' +
-              'style="display: none" class="container"/>'),
-              notifications = new models.NotificationList(),
-              env = new juju.Environment(),
-              view = new views.NotificationsView({
-                container: container,
-                notifications: notifications,
-                env: env}).render(),
-              indicator;
-
-          Y.one('body').append(container);
-          notifications.add({title: 'testing', 'level': 'error'});
-          indicator = container.one('#notify-indicator');
-
-          indicator.simulate('click');
-          indicator.ancestor().hasClass('open').should.equal(true);
-
-          Y.one('body').simulate('click');
-          indicator.ancestor().hasClass('open').should.equal(false);
-
-          container.remove();
-          done();
-        });
-      });
-
-      describe('changing notifications to words', function() {
-        var juju;
-
-        before(function() {
-          juju = Y.namespace('juju');
-        });
-
-        it('should correctly translate notification operations into English',
-           function() {
-             assert.equal(juju._changeNotificationOpToWords('add'), 'created');
-             assert.equal(juju._changeNotificationOpToWords('remove'),
-                 'removed');
-             assert.equal(juju._changeNotificationOpToWords('not-an-op'),
-                 'changed');
-           });
-      });
-
-      describe('relation notifications', function() {
-        var juju;
-
-        before(function() {
-          juju = Y.namespace('juju');
-        });
-
-        it('should produce reasonable titles', function() {
-          assert.equal(
-              juju._relationNotifications.title(undefined, 'add'),
-              'Relation created');
-          assert.equal(
-              juju._relationNotifications.title(undefined, 'remove'),
-              'Relation removed');
-        });
-
-        it('should generate messages about two-party relations', function() {
-          var changeData =
-              { endpoints:
-                    [['endpoint0', {name: 'relation0'}],
-                     ['endpoint1', {name: 'relation1'}]]};
-          assert.equal(
-              juju._relationNotifications.message(undefined, 'add',
-                  changeData), 'Relation between endpoint0 (relation type ' +
-                  '"relation0") and endpoint1 (relation type "relation1") ' +
-                  'was created');
-        });
-
-        it('should generate messages about one-party relations', function() {
-          var changeData =
-              { endpoints:
-                    [['endpoint1', {name: 'relation1'}]]};
-          assert.equal(
-              juju._relationNotifications.message(undefined, 'add',
-                  changeData), 'Relation with endpoint1 (relation type ' +
-                  '"relation1") was created');
-        });
-      });
-
-      describe('notification visual feedback', function() {
-        var env, models, notifications, notificationsView, notifierBox, views;
-
-        before(function() {
+      juju = Y.namespace('juju');
+      models = Y.namespace('juju.models');
+      views = Y.namespace('juju.views');
+      done();
+    });
+  });
+
+  it('must be able to make notification and lists of notifications',
+     function() {
+        var note1 = new models.Notification({
+         title: 'test1',
+         message: 'Hello'
+        }),
+            note2 = new models.Notification({
+         title: 'test2',
+         message: 'I said goodnight!'
+            }),
+            notifications = new models.NotificationList();
+
+        notifications.add([note1, note2]);
+        notifications.size().should.equal(2);
+
+        // timestamp should be generated once
+        var ts = note1.get('timestamp');
+        note1.get('timestamp').should.equal(ts);
+        // force an update so we can test ordering
+        // fast execution can result in same timestamp
+        note2.set('timestamp', ts + 1);
+        note2.get('timestamp').should.be.above(ts);
+
+        // defaults as expected
+        note1.get('level').should.equal('info');
+        note2.get('level').should.equal('info');
+        // the sort order on the list should be by
+        // timestamp
+        notifications.get('title').should.eql(['test2', 'test1']);
+     });
+
+  it('must be able to render its view with sample data',
+     function() {
+       var note1 = new models.Notification({
+         title: 'test1', message: 'Hello'}),
+           note2 = new models.Notification({
+         title: 'test2', message: 'I said goodnight!'}),
+           notifications = new models.NotificationList(),
+           container = Y.Node.create('<div id="test">'),
+           env = new juju.Environment(),
+           view = new views.NotificationsView({
+                   container: container,
+                   notifications: notifications,
+                   env: env});
+       view.render();
+       // Verify the expected elements appear in the view
+       container.one('#notify-list').should.not.equal(undefined);
+       container.destroy();
+     });
+
+  it('must be able to limit the size of notification events',
+     function() {
+       var note1 = new models.Notification({
+         title: 'test1',
+         message: 'Hello'
+       }),
+           note2 = new models.Notification({
+         title: 'test2',
+         message: 'I said goodnight!'
+       }),
+           note3 = new models.Notification({
+         title: 'test3',
+         message: 'Never remember'
+       }),
+           notifications = new models.NotificationList({
+         max_size: 2
+       });
+
+       notifications.add([note1, note2]);
+       notifications.size().should.equal(2);
+
+       // Adding a new notification should pop the oldest from the list (we
+       // exceed max_size)
+       notifications.add(note3);
+       notifications.size().should.equal(2);
+       notifications.get('title').should.eql(['test3', 'test2']);
+     });
+
+  it('must be able to get notifications for a given model',
+     function() {
+       var m = new models.Service({id: 'mediawiki'}),
+           note1 = new models.Notification({
+         title: 'test1',
+         message: 'Hello',
+         modelId: m
+       }),
+           note2 = new models.Notification({
+         title: 'test2',
+         message: 'I said goodnight!'
+       }),
+           notifications = new models.NotificationList();
+
+       notifications.add([note1, note2]);
+       notifications.size().should.equal(2);
+       notifications.getNotificationsForModel(m).should.eql(
+       [note1]);
+
+     });
+
+  it('must be able to include and show object links', function() {
+    var container = Y.Node.create('<div id="test">'),
+        conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
+        env = new juju.Environment({conn: conn}),
+        app = new Y.juju.App({env: env, container: container}),
+        db = app.db,
+        mw = db.services.create({id: 'mediawiki',
+                                    name: 'mediawiki'}),
+        notifications = db.notifications,
+        view = new views.NotificationsOverview({
+                      container: container,
+                      notifications: notifications,
+                      app: app,
+                      env: env}).render();
+    // we use overview here for testing as it defaults
+    // to showing all notices
+
+    // we can use app's routing table to derive a link
+    notifications.create({title: 'Service Down',
+      message: 'Your service has an error',
+      link: app.getModelURL(mw)
+    });
+    view.render();
+    var link = container.one('.notice').one('a');
+    link.getAttribute('href').should.equal(
+        '/service/mediawiki/');
+    link.getHTML().should.contain('View Details');
+
+
+    // create a new notice passing the link_title
+    notifications.create({title: 'Service Down',
+      message: 'Your service has an error',
+      link: app.getModelURL(mw),
+      link_title: 'Resolve this'
+    });
+    view.render();
+    link = container.one('.notice').one('a');
+    link.getAttribute('href').should.equal(
+        '/service/mediawiki/');
+    link.getHTML().should.contain('Resolve this');
+  });
+
+  it('must be able to evict irrelevant notices', function() {
+    var container = Y.Node.create(
+        '<div id="test" class="container"></div>'),
+        conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
+        env = new juju.Environment({conn: conn}),
+        app = new Y.juju.App({
+          env: env,
+          container: container,
+          viewContainer: container
+        });
+    var environment_delta = default_env;
+
+    var notifications = app.db.notifications,
+        view = new views.NotificationsView({
+          container: container,
+          notifications: notifications,
+          env: app.env}).render();
+
+
+    app.env.dispatch_result(environment_delta);
+
+
+    notifications.size().should.equal(7);
+    // we have one unit in error
+    view.getShowable().length.should.equal(1);
+
+    // now fire another delta event marking that node as
+    // started
+    app.env.dispatch_result({result: [['unit', 'change', {
+      'machine': 0,
+      'agent-state': 'started',
+      'public-address': '192.168.122.222',
+      'id': 'mysql/0'
+    }]], op: 'delta'});
+    notifications.size().should.equal(8);
+    // This should have evicted the prior notice from seen
+    view.getShowable().length.should.equal(0);
+  });
+
+  it('must properly construct title and message based on level from ' +
+     'event data',
+     function() {
+       var container = Y.Node.create(
+       '<div id="test" class="container"></div>'),
+       app = new Y.juju.App({
+         container: container,
+         viewContainer: container
+       });
+       var environment_delta = {
+         'result': [
+           ['service', 'add', {
+             'charm': 'cs:precise/wordpress-6',
+             'id': 'wordpress'
+           }],
+           ['service', 'add', {
+             'charm': 'cs:precise/mediawiki-3',
+             'id': 'mediawiki'
+           }],
+           ['service', 'add', {
+             'charm': 'cs:precise/mysql-6',
+             'id': 'mysql'
+           }],
+           ['unit', 'add', {
+             'agent-state': 'install-error',
+             'id': 'wordpress/0'
+           }],
+           ['unit', 'add', {
+             'agent-state': 'error',
+             'public-address': '192.168.122.222',
+             'id': 'mysql/0'
+           }],
+           ['unit', 'add', {
+             'public-address': '192.168.122.222',
+             'id': 'mysql/2'
+           }]
+         ],
+         'op': 'delta'
+       };
+
+       var notifications = app.db.notifications,
+       view = new views.NotificationsView({
+         container: container,
+         notifications: notifications,
+         app: app,
+         env: app.env}).render();
+
+       app.env.dispatch_result(environment_delta);
+
+       notifications.size().should.equal(6);
+       // we have one unit in error
+       var showable = view.getShowable();
+       showable.length.should.equal(2);
+       // The first showable notification should indicate an error.
+       showable[0].level.should.equal('error');
+       showable[0].title.should.equal('Error with mysql/0');
+       showable[0].message.should.equal('Agent-state = error.');
+       // The second showable notification should also indicate an error.
+       showable[1].level.should.equal('error');
+       showable[1].title.should.equal('Error with wordpress/0');
+       showable[1].message.should.equal('Agent-state = install-error.');
+       // The first non-error notice should have an 'info' level and less
+       // severe messaging.
+       var notice = notifications.item(0);
+       notice.get('level').should.equal('info');
+       notice.get('title').should.equal('Problem with mysql/2');
+       notice.get('message').should.equal('');
+     });
+
+
+  it('should open on click and close on clickoutside', function(done) {
+    var container = Y.Node.create(
+        '<div id="test-container" style="display: none" class="container"/>'),
+        notifications = new models.NotificationList(),
+        env = new juju.Environment(),
+        view = new views.NotificationsView({
+          container: container,
+          notifications: notifications,
+          env: env}).render(),
+        indicator;
+
+    Y.one('body').append(container);
+    notifications.add({title: 'testing', 'level': 'error'});
+    indicator = container.one('#notify-indicator');
+
+    indicator.simulate('click');
+    indicator.ancestor().hasClass('open').should.equal(true);
+
+    Y.one('body').simulate('click');
+    indicator.ancestor().hasClass('open').should.equal(false);
+
+    container.remove();
+    done();
+  });
+
+});
+
+
+describe('changing notifications to words', function() {
+  var Y, juju;
+
+  before(function(done) {
+    Y = YUI(GlobalConfig).use(
+        ['juju-notification-controller'],
+        function(Y) {
+          juju = Y.namespace('juju');
+          done();
+        });
+  });
+
+  it('should correctly translate notification operations into English',
+     function() {
+       assert.equal(juju._changeNotificationOpToWords('add'), 'created');
+       assert.equal(juju._changeNotificationOpToWords('remove'), 'removed');
+       assert.equal(juju._changeNotificationOpToWords('not-an-op'), 'changed');
+     });
+});
+
+describe('relation notifications', function() {
+  var Y, juju;
+
+  before(function(done) {
+    Y = YUI(GlobalConfig).use(
+        ['juju-notification-controller'],
+        function(Y) {
+          juju = Y.namespace('juju');
+          done();
+        });
+  });
+
+  it('should produce reasonable titles', function() {
+    assert.equal(
+        juju._relationNotifications.title(undefined, 'add'),
+        'Relation created');
+    assert.equal(
+        juju._relationNotifications.title(undefined, 'remove'),
+        'Relation removed');
+  });
+
+  it('should generate messages about two-party relations', function() {
+    var changeData =
+        { endpoints:
+              [['endpoint0', {name: 'relation0'}],
+                ['endpoint1', {name: 'relation1'}]]};
+    assert.equal(
+        juju._relationNotifications.message(undefined, 'add', changeData),
+        'Relation between endpoint0 (relation type "relation0") and ' +
+        'endpoint1 (relation type "relation1") was created');
+  });
+
+  it('should generate messages about one-party relations', function() {
+    var changeData =
+        { endpoints:
+              [['endpoint1', {name: 'relation1'}]]};
+    assert.equal(
+        juju._relationNotifications.message(undefined, 'add', changeData),
+        'Relation with endpoint1 (relation type "relation1") was created');
+  });
+});
+
+describe('notification visual feedback', function() {
+  var env, models, notifications, notificationsView, notifierBox, views, Y;
+
+  before(function(done) {
+    Y = YUI(GlobalConfig).use('juju-env', 'juju-models', 'juju-views',
+        function(Y) {
           var juju = Y.namespace('juju');
           env = new juju.Environment();
           models = Y.namespace('juju.models');
           views = Y.namespace('juju.views');
-        });
-
-        // Instantiate the notifications model list and view.
-        // Also create the notifier box and attach it as first element of the
-        // body.
-        beforeEach(function() {
-          notifications = new models.NotificationList();
-          notificationsView = new views.NotificationsView({
-            env: env,
-            notifications: notifications
-          });
-          notifierBox = Y.Node.create('<div id="notifier-box"></div>');
-          notifierBox.setStyle('display', 'none');
-          Y.one('body').prepend(notifierBox);
-        });
-
-        // Destroy the notifier box created in beforeEach.
-        afterEach(function() {
-          notifierBox.remove();
-          notifierBox.destroy(true);
-        });
-
-        // Assert the notifier box contains the expectedNumber of notifiers.
-        var assertNumNotifiers = function(expectedNumber) {
-          assert.equal(expectedNumber, notifierBox.get('children').size());
-        };
-
-        it('should appear when a new error is notified', function() {
-          notifications.add({title: 'mytitle', level: 'error'});
-          assertNumNotifiers(1);
-        });
-
-        it('should only appear when the DOM contains the notifier box',
-            function() {
-             notifierBox.remove();
-             notifications.add({title: 'mytitle', level: 'error'});
-             assertNumNotifiers(0);
-           });
-
-        it('should not appear when the notification is not an error',
-            function() {
-             notifications.add({title: 'mytitle', level: 'info'});
-             assertNumNotifiers(0);
-           });
-
-        it('should not appear when the notification comes form delta',
-            function() {
-             notifications.add({title: 'mytitle', level: 'error', isDelta:
-               true});
-             assertNumNotifiers(0);
-           });
-
-      });
+          done();
+        });
+  });
+
+  // Instantiate the notifications model list and view.
+  // Also create the notifier box and attach it as first element of the body.
+  beforeEach(function() {
+    notifications = new models.NotificationList();
+    notificationsView = new views.NotificationsView({
+      env: env,
+      notifications: notifications
     });
+    notifierBox = Y.Node.create('<div id="notifier-box"></div>');
+    notifierBox.setStyle('display', 'none');
+    Y.one('body').prepend(notifierBox);
+  });
+
+  // Destroy the notifier box created in beforeEach.
+  afterEach(function() {
+    notifierBox.remove();
+    notifierBox.destroy(true);
+  });
+
+  // Assert the notifier box contains the expectedNumber of notifiers.
+  var assertNumNotifiers = function(expectedNumber) {
+    assert.equal(expectedNumber, notifierBox.get('children').size());
+  };
+
+  it('should appear when a new error is notified', function() {
+    notifications.add({title: 'mytitle', level: 'error'});
+    assertNumNotifiers(1);
+  });
+
+  it('should only appear when the DOM contains the notifier box', function() {
+    notifierBox.remove();
+    notifications.add({title: 'mytitle', level: 'error'});
+    assertNumNotifiers(0);
+  });
+
+  it('should not appear when the notification is not an error', function() {
+    notifications.add({title: 'mytitle', level: 'info'});
+    assertNumNotifiers(0);
+  });
+
+  it('should not appear when the notification comes form delta', function() {
+    notifications.add({title: 'mytitle', level: 'error', isDelta: true});
+    assertNumNotifiers(0);
+  });
+
+});

=== modified file 'test/test_notifier_widget.js'
--- test/test_notifier_widget.js	2012-11-23 16:21:32 +0000
+++ test/test_notifier_widget.js	2013-01-07 15:36:01 +0000
@@ -1,101 +1,104 @@
 'use strict';
 
-YUI(GlobalConfig).use(['notifier', 'node-event-simulate'], function(Y) {
-  describe('notifier widget', function() {
-    var Notifier, notifierBox;
-
-    before(function() {
-      Notifier = Y.namespace('juju.widgets').Notifier;
-    });
-
-    // Create the notifier box and attach it as first element of the body.
-    beforeEach(function() {
-      notifierBox = Y.Node.create('<div id="notifier-box"></div>');
-      notifierBox.setStyle('display', 'none');
-      Y.one('body').prepend(notifierBox);
-    });
-
-    // Destroy the notifier box created in beforeEach.
-    afterEach(function() {
-      notifierBox.remove();
-      notifierBox.destroy(true);
-    });
-
-    // Factory rendering and returning a notifier instance.
-    var makeNotifier = function(title, message, timeout) {
-      var notifier = new Notifier({
-        title: title || 'mytitle',
-        message: message || 'mymessage',
-        timeout: timeout || 10000
-      });
-      notifier.render(notifierBox);
-      return notifier;
-    };
-
-    // Assert the notifier box contains the expectedNumber of notifiers.
-    var assertNumNotifiers = function(expectedNumber) {
-      assert.equal(expectedNumber, notifierBox.get('children').size());
-    };
-
-    it('should be able to display a notification', function() {
+
+describe('notifier widget', function() {
+  var Notifier, notifierBox, Y;
+
+  before(function(done) {
+    Y = YUI(GlobalConfig).use(['notifier', 'node-event-simulate'], function(Y) {
+          Notifier = Y.namespace('juju.widgets').Notifier;
+          done();
+    });
+  });
+
+  // Create the notifier box and attach it as first element of the body.
+  beforeEach(function() {
+    notifierBox = Y.Node.create('<div id="notifier-box"></div>');
+    notifierBox.setStyle('display', 'none');
+    Y.one('body').prepend(notifierBox);
+  });
+
+  // Destroy the notifier box created in beforeEach.
+  afterEach(function() {
+    notifierBox.remove();
+    notifierBox.destroy(true);
+  });
+
+  // Factory rendering and returning a notifier instance.
+  var makeNotifier = function(title, message, timeout) {
+    var notifier = new Notifier({
+      title: title || 'mytitle',
+      message: message || 'mymessage',
+      timeout: timeout || 10000
+    });
+    notifier.render(notifierBox);
+    return notifier;
+  };
+
+  // Assert the notifier box contains the expectedNumber of notifiers.
+  var assertNumNotifiers = function(expectedNumber) {
+    assert.equal(expectedNumber, notifierBox.get('children').size());
+  };
+
+  it('should be able to display a notification', function() {
+    makeNotifier();
+    assertNumNotifiers(1);
+  });
+
+  it('should display the given title and message', function() {
+    makeNotifier('mytitle', 'mymessage');
+    var notifierNode = notifierBox.one('*');
+    assert.equal('mytitle', notifierNode.one('h3').getContent());
+    assert.equal('mymessage', notifierNode.one('div').getContent());
+  });
+
+  it('should be able to display multiple notifications', function() {
+    var number = 10;
+    for (var i = 0; i < number; i += 1) {
       makeNotifier();
+    }
+    assertNumNotifiers(number);
+  });
+
+  it('should display new notifications on top', function() {
+    makeNotifier('mytitle1', 'mymessage1');
+    makeNotifier('mytitle2', 'mymessage2');
+    var notifierNode = notifierBox.one('*');
+    assert.equal('mytitle2', notifierNode.one('h3').getContent());
+    assert.equal('mymessage2', notifierNode.one('div').getContent());
+  });
+
+  it('should destroy notifications after N milliseconds', function(done) {
+    makeNotifier('mytitle', 'mymessage', 1);
+    // A timeout of 250 milliseconds is used so that we ensure the destroying
+    // animation can be completed.
+    setTimeout(function() {
+      assertNumNotifiers(0);
+      done();
+    }, 250);
+  });
+
+  it('should destroy notifications on click', function(done) {
+    makeNotifier();
+    notifierBox.one('*').simulate('click');
+    // A timeout of 250 milliseconds is used so that we ensure the destroying
+    // animation can be completed.
+    setTimeout(function() {
+      assertNumNotifiers(0);
+      done();
+    }, 250);
+  });
+
+  it('should prevent notification removal on mouse enter', function(done) {
+    makeNotifier('mytitle', 'mymessage', 1);
+    notifierBox.one('*').simulate('mouseover');
+    // A timeout of 250 milliseconds is used so that we ensure the node is not
+    // preserved by the destroying animation.
+    setTimeout(function() {
       assertNumNotifiers(1);
-    });
-
-    it('should display the given title and message', function() {
-      makeNotifier('mytitle', 'mymessage');
-      var notifierNode = notifierBox.one('*');
-      assert.equal('mytitle', notifierNode.one('h3').getContent());
-      assert.equal('mymessage', notifierNode.one('div').getContent());
-    });
-
-    it('should be able to display multiple notifications', function() {
-      var number = 10;
-      for (var i = 0; i < number; i += 1) {
-        makeNotifier();
-      }
-      assertNumNotifiers(number);
-    });
-
-    it('should display new notifications on top', function() {
-      makeNotifier('mytitle1', 'mymessage1');
-      makeNotifier('mytitle2', 'mymessage2');
-      var notifierNode = notifierBox.one('*');
-      assert.equal('mytitle2', notifierNode.one('h3').getContent());
-      assert.equal('mymessage2', notifierNode.one('div').getContent());
-    });
-
-    it('should destroy notifications after N milliseconds', function(done) {
-      makeNotifier('mytitle', 'mymessage', 1);
-      // A timeout of 250 milliseconds is used so that we ensure the destroying
-      // animation can be completed.
-      setTimeout(function() {
-        assertNumNotifiers(0);
-        done();
-      }, 250);
-    });
-
-    it('should destroy notifications on click', function(done) {
-      makeNotifier();
-      notifierBox.one('*').simulate('click');
-      // A timeout of 250 milliseconds is used so that we ensure the destroying
-      // animation can be completed.
-      setTimeout(function() {
-        assertNumNotifiers(0);
-        done();
-      }, 250);
-    });
-
-    it('should prevent notification removal on mouse enter', function(done) {
-      makeNotifier('mytitle', 'mymessage', 1);
-      notifierBox.one('*').simulate('mouseover');
-      // A timeout of 250 milliseconds is used so that we ensure the node is not
-      // preserved by the destroying animation.
-      setTimeout(function() {
-        assertNumNotifiers(1);
-        done();
-      }, 250);
-    });
-
+      done();
+    }, 250);
   });
+
 });
+

=== modified file 'test/test_panzoom.js'
--- test/test_panzoom.js	2013-01-02 12:57:09 +0000
+++ test/test_panzoom.js	2013-01-07 15:36:01 +0000
@@ -26,7 +26,7 @@
     db = new models.Database();
     var view = new views.environment({container: viewContainer, db: db});
     view.render();
-    view.postRender();
+    view.rendered();
     pz = view.topo.modules.PanZoomModule;
     topo = pz.get('component');
     vis = topo.vis;

=== modified file 'test/test_topology.js'
--- test/test_topology.js	2012-12-19 13:45:10 +0000
+++ test/test_topology.js	2013-01-07 15:36:01 +0000
@@ -81,6 +81,7 @@
     topo.setAttrs({container: container, db: db});
     topo.addModule(views.MegaModule);
     topo.addModule(views.PanZoomModule);
+    topo.addModule(views.ViewportModule);
     return topo;
   }
 

=== modified file 'undocumented'
--- undocumented	2012-12-21 21:31:21 +0000
+++ undocumented	2013-01-07 15:36:01 +0000
@@ -1,19 +1,5 @@
 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"
@@ -40,15 +26,52 @@
 app/store/env.js:86 "dispatch_result"
 app/store/env.js:139 "add_relation"
 app/store/env.js:22 "initializer"
-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/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:370 "_addAlertMessage"
+app/views/utils.js:825 "BoxPair"
+app/views/utils.js:702 "scale"
+app/views/utils.js:227 "humanizeNumber"
+app/views/utils.js:137 "console"
+app/views/utils.js:828 "pair"
+app/views/utils.js:113 "noop"
+app/views/utils.js:617 "Box"
+app/views/utils.js:250 "hasSVGClass"
+app/views/utils.js:338 "action"
+app/views/utils.js:134 "noop"
+app/views/utils.js:658 "get"
+app/views/utils.js:217 "renderable_charm"
+app/views/utils.js:651 "set"
+app/views/utils.js:641 "get"
+app/views/utils.js:566 "isInt"
+app/views/utils.js:615 "BoundingBox"
+app/views/utils.js:570 "isFloat"
+app/views/utils.js:650 "get"
+app/views/utils.js:703 "translate"
+app/views/utils.js:418 "invokeCallback"
+app/views/utils.js:559 "toString"
+app/views/utils.js:295 "toggleSVGClass"
+app/views/utils.js:258 "addSVGClass"
+app/views/utils.js:659 "set"
+app/views/utils.js:279 "removeSVGClass"
+app/views/utils.js:194 "bindModelView"
+app/views/utils.js:165 "substitute"
+app/views/utils.js:644 "set"
+app/views/utils.js:131 "native"
+app/views/environment.js:24 "initializer"
+app/views/environment.js:31 "render"
 app/views/charm-panel.js:1168 "calculatePanelPosition"
 app/views/charm-panel.js:476 "initializer"
 app/views/charm-panel.js:250 "render"
@@ -71,6 +94,15 @@
 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"
@@ -92,36 +124,6 @@
 app/views/unit.js:297 "retryRelation"
 app/views/unit.js:118 "confirmResolved"
 app/views/unit.js:23 "initializer"
-app/views/utils.js:642 "set"
-app/views/utils.js:225 "humanizeNumber"
-app/views/utils.js:135 "console"
-app/views/utils.js:132 "noop"
-app/views/utils.js:293 "toggleSVGClass"
-app/views/utils.js:613 "BoundingBox"
-app/views/utils.js:639 "get"
-app/views/utils.js:336 "action"
-app/views/utils.js:615 "Box"
-app/views/utils.js:648 "get"
-app/views/utils.js:113 "noop"
-app/views/utils.js:277 "removeSVGClass"
-app/views/utils.js:657 "set"
-app/views/utils.js:649 "set"
-app/views/utils.js:368 "_addAlertMessage"
-app/views/utils.js:557 "toString"
-app/views/utils.js:163 "substitute"
-app/views/utils.js:564 "isInt"
-app/views/utils.js:826 "pair"
-app/views/utils.js:248 "hasSVGClass"
-app/views/utils.js:656 "get"
-app/views/utils.js:700 "scale"
-app/views/utils.js:215 "renderable_charm"
-app/views/utils.js:568 "isFloat"
-app/views/utils.js:823 "BoxPair"
-app/views/utils.js:416 "invokeCallback"
-app/views/utils.js:192 "bindModelView"
-app/views/utils.js:129 "native"
-app/views/utils.js:701 "translate"
-app/views/utils.js:256 "addSVGClass"
 app/views/service.js:488 "updateConstraints"
 app/views/service.js:514 "_setConstraintsCallback"
 app/views/service.js:846 "filterUnits"
@@ -151,22 +153,44 @@
 app/views/service.js:23 "resetUnits"
 app/views/service.js:230 "unexposeService"
 app/views/service.js:237 "_unexposeServiceCallback"
-app/views/environment.js:24 "initializer"
-app/views/environment.js:31 "render"
-app/views/environment.js:64 "postRender"
+app/views/topology/mega.js:373 "drawService"
+app/views/topology/mega.js:749 "destroyServiceConfirm"
+app/views/topology/mega.js:152 "serviceClick"
+app/views/topology/mega.js:170 "serviceDblClick"
+app/views/topology/mega.js:273 "update"
+app/views/topology/mega.js:720 "toggleControlPanel"
+app/views/topology/mega.js:609 "fade"
+app/views/topology/mega.js:597 "show"
+app/views/topology/mega.js:195 "serviceMouseLeave"
+app/views/topology/mega.js:740 "show_service"
+app/views/topology/mega.js:781 "_destroyCallback"
+app/views/topology/mega.js:671 "hideGraphListPicker"
+app/views/topology/mega.js:177 "serviceMouseEnter"
+app/views/topology/mega.js:628 "renderedHandler"
+app/views/topology/mega.js:772 "destroyService"
+app/views/topology/mega.js:603 "hide"
+app/views/topology/mega.js:142 "initializer"
+app/views/topology/mega.js:661 "showGraphListPicker"
+app/views/topology/mega.js:678 "updateServiceMenuLocation"
+app/views/topology/mega.js:233 "updateData"
+app/views/topology/panzoom.js:100 "_fire_zoom"
+app/views/topology/panzoom.js:71 "zoomHandler"
+app/views/topology/panzoom.js:132 "rescale"
+app/views/topology/panzoom.js:33 "initializer"
+app/views/topology/panzoom.js:39 "renderSlider"
+app/views/topology/panzoom.js:153 "renderedHandler"
+app/views/topology/panzoom.js:82 "zoom_out"
+app/views/topology/panzoom.js:91 "zoom_in"
 app/views/topology/relation.js:78 "processRelation"
 app/views/topology/relation.js:403 "_removeRelationCallback"
 app/views/topology/relation.js:325 "addRelationDragStart"
+app/views/topology/relation.js:460 "cancelRelationBuild"
 app/views/topology/relation.js:391 "removeRelation"
 app/views/topology/relation.js:309 "snapOutOfService"
-app/views/topology/relation.js:448 "cancelRelationBuild"
-app/views/topology/relation.js:620 "addRelationEnd"
-app/views/topology/relation.js:721 "subRelBlockMouseLeave"
-app/views/topology/relation.js:712 "subRelBlockMouseEnter"
+app/views/topology/relation.js:724 "subRelBlockMouseEnter"
+app/views/topology/relation.js:632 "addRelationEnd"
 app/views/topology/relation.js:350 "addRelationDrag"
 app/views/topology/relation.js:280 "snapToService"
-app/views/topology/relation.js:705 "subordinateRelationsForService"
-app/views/topology/relation.js:524 "addRelationStart"
 app/views/topology/relation.js:251 "draglineClicked"
 app/views/topology/relation.js:158 "drawRelationGroup"
 app/views/topology/relation.js:112 "updateLinks"
@@ -175,71 +199,34 @@
 app/views/topology/relation.js:55 "render"
 app/views/topology/relation.js:270 "addRelation"
 app/views/topology/relation.js:257 "addRelButtonClicked"
-app/views/topology/relation.js:661 "_addRelationCallback"
+app/views/topology/relation.js:781 "relationClick"
+app/views/topology/relation.js:673 "_addRelationCallback"
+app/views/topology/relation.js:717 "subordinateRelationsForService"
 app/views/topology/relation.js:372 "addRelationDragEnd"
 app/views/topology/relation.js:90 "processRelations"
-app/views/topology/relation.js:769 "relationClick"
 app/views/topology/relation.js:50 "initializer"
-app/views/topology/relation.js:536 "ambiguousAddRelationCheck"
+app/views/topology/relation.js:536 "addRelationStart"
+app/views/topology/relation.js:733 "subRelBlockMouseLeave"
 app/views/topology/relation.js:60 "update"
 app/views/topology/relation.js:221 "drawRelation"
 app/views/topology/relation.js:74 "renderedHandler"
-app/views/topology/panzoom.js:83 "update"
-app/views/topology/panzoom.js:40 "zoomHandler"
-app/views/topology/panzoom.js:100 "zoom_in"
-app/views/topology/panzoom.js:136 "rescale"
-app/views/topology/panzoom.js:109 "_fire_zoom"
-app/views/topology/panzoom.js:33 "initializer"
-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/mega.js:614 "fade"
-app/views/topology/mega.js:200 "serviceMouseLeave"
-app/views/topology/mega.js:278 "update"
-app/views/topology/mega.js:779 "toggleControlPanel"
-app/views/topology/mega.js:799 "show_service"
-app/views/topology/mega.js:679 "hideGraphListPicker"
-app/views/topology/mega.js:182 "serviceMouseEnter"
-app/views/topology/mega.js:175 "serviceDblClick"
-app/views/topology/mega.js:808 "destroyServiceConfirm"
-app/views/topology/mega.js:147 "initializer"
-app/views/topology/mega.js:608 "hide"
-app/views/topology/mega.js:669 "showGraphListPicker"
-app/views/topology/mega.js:689 "setSizesFromViewport"
-app/views/topology/mega.js:831 "destroyService"
-app/views/topology/mega.js:378 "drawService"
-app/views/topology/mega.js:633 "renderedHandler"
-app/views/topology/mega.js:737 "updateServiceMenuLocation"
-app/views/topology/mega.js:238 "updateData"
-app/views/topology/mega.js:602 "show"
-app/views/topology/mega.js:840 "_destroyCallback"
-app/views/topology/mega.js:157 "serviceClick"
-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/relation.js:548 "ambiguousAddRelationCheck"
 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/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/views/topology/topology.js:166 "getter"
+app/views/topology/topology.js:159 "getter"
+app/views/topology/topology.js:167 "setter"
+app/views/topology/topology.js:175 "getter"
+app/views/topology/topology.js:104 "computeScales"
+app/views/topology/topology.js:63 "renderOnce"
+app/views/topology/topology.js:136 "serviceForBox"
+app/views/topology/topology.js:171 "getter"
+app/views/topology/topology.js:26 "initializer"
+app/views/topology/topology.js:160 "setter"
+app/views/topology/viewport.js:38 "resized"
 app/models/models.js:305 "setter"
 app/models/models.js:430 "getModelListByModelName"
 app/models/models.js:325 "add"
@@ -264,3 +251,11 @@
 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