← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/maas-add-node-js3 into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/maas-add-node-js3 into lp:maas with lp:~rvb/maas/maas-add-node-js2 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/maas-add-node-js3/+merge/91440

This branch adds a Dashboard view used on the index page.  The dashboard is pretty minimal right now, it simply displays the number of nodes.

- added the Dashboard view which preloads the visible nodes and the listens to the 'nodeAdded' event.
- renamed add_node to node_add (YUI module name, files, etc).
- added node.js with models for Node and NodeList.  This is pretty minimal right now but it's all we need to count the nodes.
- added testing.js which contains a base class for tests that need to mockup io or cleanup event handlers.  I assumes that the io provided is stored in module._io, this is not really nice but I think it's better than to modify the modules themself for the sole purpose of being able to test them. test_node_add.js has been cleaned up to take advantage of this.
-- 
https://code.launchpad.net/~rvb/maas/maas-add-node-js3/+merge/91440
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-add-node-js3 into lp:maas.
=== added file 'src/maasserver/static/js/node.js'
--- src/maasserver/static/js/node.js	1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/node.js	2012-02-03 14:44:20 +0000
@@ -0,0 +1,43 @@
+/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Node model.
+ *
+ * @module Y.mass.node
+ */
+
+YUI.add('maas.node', function(Y) {
+
+Y.log('loading mass.node');
+var module = Y.namespace('maas.node');
+
+/**
+ * A Y.Model to represent a Node.
+ *
+ */
+module.Node = Y.Base.create('nodeModel', Y.Model, [], {
+    idAttribute: 'system_id'
+}, {
+    ATTRS: {
+        system_id: {
+        },
+        hostname: {
+        },
+        status: {
+        },
+        after_commissioning_action: {
+        }
+    }
+});
+
+/**
+ * A Y.ModelList that is meant to contain instances of Y.maas.node.Node.
+ *
+ */
+module.NodeList = Y.Base.create('nodeList', Y.ModelList, [], {
+    model: module.Node
+
+});
+
+}, '0.1', {'requires': ['model', 'model-list']}
+);

=== renamed file 'src/maasserver/static/js/add_node.js' => 'src/maasserver/static/js/node_add.js'
--- src/maasserver/static/js/add_node.js	2012-02-02 16:50:18 +0000
+++ src/maasserver/static/js/node_add.js	2012-02-03 14:44:20 +0000
@@ -3,14 +3,14 @@
  *
  * Widget to add a Node.
  *
- * @module Y.mass.add_node
+ * @module Y.mass.node_add
  */
 
-YUI.add('maas.add_node', function(Y) {
-
-Y.log('loading mass.add_node');
-
-var module = Y.namespace('maas.add_node');
+YUI.add('maas.node_add', function(Y) {
+
+Y.log('loading mass.node_add');
+
+var module = Y.namespace('maas.node_add');
 
 module.NODE_ADDED_EVENT = 'nodeAdded';
 
@@ -21,7 +21,7 @@
     AddNodeWidget.superclass.constructor.apply(this, arguments);
 };
 
-AddNodeWidget.NAME = 'add-node-widget';
+AddNodeWidget.NAME = 'node-add-widget';
 
 AddNodeWidget.ATTRS = {
 
@@ -118,10 +118,10 @@
      },
 
    /**
-     * Show the spinner.
-     *
-     * @method showSpinner
-     */
+    * Show the spinner.
+    *
+    * @method showSpinner
+    */
     showSpinner: function() {
         var button = this.get('srcNode').one('.add-node-button');
         button.insert(this.spinnerNode, 'after');

=== added file 'src/maasserver/static/js/node_views.js'
--- src/maasserver/static/js/node_views.js	1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/node_views.js	2012-02-03 14:44:20 +0000
@@ -0,0 +1,192 @@
+/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * Node model.
+ *
+ * @module Y.mass.node
+ */
+
+YUI.add('maas.node_views', function(Y) {
+
+Y.log('loading mass.node_views');
+var module = Y.namespace('maas.node_views');
+
+// Only used to mockup io in tests.
+module._io = Y;
+
+/**
+ * A base view class to display a set of Nodes (Y.maas.node.Node).
+ *
+ * It will load the list of visible nodes (in this.modelList) when rendered
+ * for the first time and be subscribed to 'nodeAdded' events published by
+ * Y.maas.node_add.AddNodeDispatcher.  Changes to this.modelList will trigger
+ * re-rendering.
+ *
+ * You can provide your custom rendering method by defining a 'display'
+ * method (also, you can provide methods named 'loadNodesStarted' and
+ * 'loadNodesEnded' to customize the display during the initial loading of the
+ * visible nodes and a method named 'displayGlobalError' to display a message
+ * when errors occur during loading).
+ *
+ */
+module.NodeListLoader = Y.Base.create('nodeListLoader', Y.View, [], {
+
+    initializer: function(config) {
+        this.modelList = new Y.maas.node.NodeList();
+        this.nodes_loaded = false;
+        this.handle = this.registerAddNodeDispatcher(
+            Y.maas.node_add.AddNodeDispatcher);
+    },
+
+    destructor: function() {
+        this.handle.detach();
+    },
+
+    render: function () {
+        if (this.nodes_loaded) {
+            this.display();
+        }
+        else {
+            this.loadNodesAndRender();
+        }
+    },
+
+    registerAddNodeDispatcher: function(dispatcher) {
+        return dispatcher.on(
+            Y.maas.node_add.NODE_ADDED_EVENT,
+            function(e, node) {
+                this.modelList.add(node);
+            },
+            this);
+    },
+
+   /**
+    * Load visible Nodes (store them in this.modelList) and render this view.
+    * to be populated.
+    *
+    * @method loadNodesAndRender
+    */
+    loadNodesAndRender: function() {
+        var self = this;
+        var cfg = {
+            method: 'GET',
+            sync: false,
+            on: {
+                start: Y.bind(self.loadNodesStarted, self),
+                success: function(id, out) {
+                    var node_data;
+                    try {
+                        node_data = JSON.parse(out.response);
+                    }
+                    catch(e) {
+                        // Parsing error.
+                        self.displayGlobalError('Unable to load nodes.');
+                     }
+                    self.modelList.add(node_data);
+                    self.modelList.after(
+                        ['add', 'remove', 'reset'], self.render, self);
+                    self.nodes_loaded = true;
+                    self.display();
+                },
+                failure: function(id, out) {
+                    // Unexpected error.
+                    self.displayGlobalError('Unable to load nodes.');
+                },
+                end: Y.bind(self.loadNodesEnded, self)
+            }
+        };
+        var request = module._io.io(
+            MAAS_config.uris.nodes_handler, cfg);
+    },
+
+   /**
+    * Function called when rendering occurs.  this.modelList is guaranteed
+    * to be populated.
+    *
+    * @method display
+    */
+    display: function () {
+    },
+
+   /**
+    * Function called if an error occurs during the initial node loading.
+    * to be populated.
+    *
+    * @method display
+    */
+    displayGlobalError: function (error_message) {
+    },
+
+   /**
+    * Function called when the Node list starts loading.
+    *
+    * @method loadNodesStarted
+    */
+    loadNodesStarted: function() {
+    },
+
+   /**
+    * Function called when the Node list has loaded.
+    *
+    * @method loadNodesEnded
+    */
+    loadNodesEnded: function() {
+    }
+
+});
+
+/**
+ * A customized view based on NodeListLoader that will display a dashboard
+ * of the nodes.
+ *
+ * @method display
+ */
+module.NodesDashboard = Y.Base.create(
+    'nodesDashboard', module.NodeListLoader, [], {
+
+    plural_template: (
+      '<h2>{nb_nodes} nodes in this cluster</h2><div id="chart" />'),
+    singular_template: (
+        '<h2>{nb_nodes} node in this cluster</h2><div id="chart" />'),
+
+    initializer: function(config) {
+        this.append = config.append;
+       // Prepare spinnerNode.
+        this.spinnerNode = Y.Node.create('<img />')
+            .set('src', MAAS_config.uris.statics + 'img/spinner.gif');
+    },
+
+   /**
+    * Display a dashboard of the nodes (right now a simple count).
+    *
+    * @method display
+    */
+    display: function () {
+        var size = this.modelList.size();
+        var template;
+        if (size === 1) {
+            template = this.singular_template;
+        }
+        else {
+        template = this.plural_template;
+        }
+        Y.one(this.container).setContent(
+            Y.Lang.sub(template, {nb_nodes: size}));
+
+        if (!this.container.inDoc()) {
+            Y.one(this.append).empty().append(this.container, 0);
+        }
+    },
+
+    loadNodesStarted: function() {
+        Y.one(this.append).insert(this.spinnerNode, 0);
+    },
+
+    loadNodesEnded: function() {
+        this.spinnerNode.remove();
+    }
+
+});
+
+}, '0.1', {'requires': ['view', 'io', 'maas.node', 'maas.node_add']}
+);

=== added file 'src/maasserver/static/js/testing/testing.js'
--- src/maasserver/static/js/testing/testing.js	1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/testing/testing.js	2012-02-03 14:44:20 +0000
@@ -0,0 +1,76 @@
+/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ */
+
+YUI().add('maas.testing', function(Y) {
+
+Y.log('loading mass.testing');
+
+var module = Y.namespace('maas.testing');
+
+module.TestCase = Y.Base.create('ioMockableTestCase', Y.Test.Case, [], {
+
+   /**
+    * Mock the '_io' field of the provided module.  This assumes that
+    * the module has a internal reference to its io module named '_io'
+    * and that all its io is done via module._io.io(...).
+    *
+    * @method mockIO
+    * @param mock the mock object that should replace the module's io
+    * @param module the module to monkey patch
+    */
+    mockIO: function(mock, module) {
+        this.old_io = module._io;
+        this.module = module;
+        this.module._io = mock;
+    },
+
+    tearDown: function() {
+        if (Y.Lang.isValue(this.old_io)) {
+            this.module._io = this.old_io;
+        }
+        if (Y.Lang.isValue(this.handlers)) {
+            var handler;
+            while(handler=this.handlers.pop()) {
+                handler.detach();
+            }
+        }
+    },
+
+    mockSuccess: function(response, module) {
+        var mockXhr = {};
+        mockXhr.io = function(url, cfg) {
+           var out = {};
+           out.response = response;
+           cfg.on.success('4', out);
+        };
+        this.mockIO(mockXhr, module);
+    },
+
+   /**
+    * Register a method to be fired when the event 'name' is triggered on
+    * 'source'.  The handle will be cleaned up when the test finishes.
+    *
+    * @method registerListener
+    * @param source the source of the event
+    * @param name the name of the event to listen to
+    * @param method the method to run
+    * @param context the context in which the method should be run
+    */
+    registerListener: function(source, name, method, context) {
+        var handle = source.on(name, method, context);
+        this.cleanupHandler(handle);
+        return handle;
+    },
+
+    cleanupHandler: function(handler) {
+        if (!Y.Lang.isValue(this.handlers)) {
+            this.handlers = [];
+        }
+        this.handlers.push(handler);
+    }
+
+});
+
+}, '0.1', {'requires': ['test', 'base']}
+);

=== modified file 'src/maasserver/static/js/testing/testrunner.js'
--- src/maasserver/static/js/testing/testrunner.js	2012-02-02 16:50:18 +0000
+++ src/maasserver/static/js/testing/testrunner.js	2012-02-03 14:44:20 +0000
@@ -1,3 +1,7 @@
+/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ */
+
 /**
  * Merely loading this script into a page will cause it to look for a
  * single suite using the selector span#suite. If found, the text

=== added file 'src/maasserver/static/js/tests/test_node.html'
--- src/maasserver/static/js/tests/test_node.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/tests/test_node.html	2012-02-03 14:44:20 +0000
@@ -0,0 +1,17 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
+<html xmlns="http://www.w3.org/1999/xhtml"; xml:lang="en" lang="en">
+  <head>
+    <title>Test maas.node</title>
+
+    <!-- YUI and test setup -->
+    <script type="text/javascript" src="../yui/tests/yui/yui-min.js"></script>
+    <script type="text/javascript" src="../testing/testrunner.js"></script>
+    <!-- The module under test -->
+    <script type="text/javascript" src="../node.js"></script>
+    <!-- The test suite -->
+    <script type="text/javascript" src="test_node.js"></script>
+  </head>
+  <body>
+  <span id="suite">maas.node.tests</span>
+  </body>
+</html>

=== added file 'src/maasserver/static/js/tests/test_node.js'
--- src/maasserver/static/js/tests/test_node.js	1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/tests/test_node.js	2012-02-03 14:44:20 +0000
@@ -0,0 +1,33 @@
+/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ */
+
+YUI({ useBrowserConsole: true }).add('maas.node.tests', function(Y) {
+
+Y.log('loading mass.node.tests');
+var namespace = Y.namespace('maas.node.tests');
+
+var module = Y.maas.node;
+var suite = new Y.Test.Suite("maas.node Tests");
+
+suite.add(new Y.Test.Case({
+    name: 'test-node',
+
+    testNode: function() {
+        var node = new module.Node({'system_id': '5'});
+        Y.Assert.areSame(node.idAttribute, 'system_id');
+        Y.Assert.areSame('5', node.get('system_id'));
+    },
+
+    testNodeList: function() {
+        var node_list = new module.NodeList();
+        Y.Assert.areSame(module.Node, node_list.model);
+    }
+
+}));
+
+namespace.suite = suite;
+
+}, '0.1', {'requires': [
+    'node-event-simulate', 'test', 'maas.node']}
+);

=== renamed file 'src/maasserver/static/js/tests/test_add_node.html' => 'src/maasserver/static/js/tests/test_node_add.html'
--- src/maasserver/static/js/tests/test_add_node.html	2012-02-02 16:50:18 +0000
+++ src/maasserver/static/js/tests/test_node_add.html	2012-02-03 14:44:20 +0000
@@ -1,8 +1,16 @@
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
 <html xmlns="http://www.w3.org/1999/xhtml"; xml:lang="en" lang="en">
   <head>
-    <title>Test maas.add_node</title>
+    <title>Test maas.node_add</title>
 
+    <!-- YUI and test setup -->
+    <script type="text/javascript" src="../yui/tests/yui/yui-min.js"></script>
+    <script type="text/javascript" src="../testing/testrunner.js"></script>
+    <script type="text/javascript" src="../testing/testing.js"></script>
+    <!-- The module under test -->
+    <script type="text/javascript" src="../node_add.js"></script>
+    <!-- The test suite -->
+    <script type="text/javascript" src="test_node_add.js"></script>
     <script type="text/javascript">
     <!--
     var MAAS_config = {
@@ -13,13 +21,6 @@
     };
     // -->
     </script>
-    <!-- YUI and test setup -->
-    <script type="text/javascript" src="../yui/tests/yui/yui-min.js"></script>
-    <script type="text/javascript" src="../testing/testrunner.js"></script>
-    <!-- The module under test -->
-    <script type="text/javascript" src="../add_node.js"></script>
-    <!-- The test suite -->
-    <script type="text/javascript" src="test_add_node.js"></script>
   </head>
   <body>
   <script type="text/x-template" id="add-macaddress">
@@ -34,6 +35,6 @@
     </p>
   </script>
 
-  <span id="suite">maas.add_node.tests</span>
+  <span id="suite">maas.node_add.tests</span>
   </body>
 </html>

=== renamed file 'src/maasserver/static/js/tests/test_add_node.js' => 'src/maasserver/static/js/tests/test_node_add.js'
--- src/maasserver/static/js/tests/test_add_node.js	2012-02-02 19:04:24 +0000
+++ src/maasserver/static/js/tests/test_node_add.js	2012-02-03 14:44:20 +0000
@@ -2,16 +2,16 @@
  * GNU Affero General Public License version 3 (see the file LICENSE).
  */
 
-YUI({ useBrowserConsole: true }).add('maas.add_node.tests', function(Y) {
-
-Y.log('loading mass.add_node.tests');
-var namespace = Y.namespace('maas.add_node.tests');
-
-var module = Y.maas.add_node;
-var suite = new Y.Test.Suite("maas.add_node Tests");
-
-suite.add(new Y.Test.Case({
-    name: 'test-add-node-widget-singleton',
+YUI({ useBrowserConsole: true }).add('maas.node_add.tests', function(Y) {
+
+Y.log('loading mass.node_add.tests');
+var namespace = Y.namespace('maas.node_add.tests');
+
+var module = Y.maas.node_add;
+var suite = new Y.Test.Suite("maas.node_add Tests");
+
+suite.add(new Y.maas.testing.TestCase({
+    name: 'test-node-add-widget-singleton',
 
     setUp: function() {
         // Silence io.
@@ -20,12 +20,7 @@
             method: 'io',
             args: [MAAS_config.uris.nodes_handler, Y.Mock.Value.Any]
         });
-        this.old_io = module._io;
-        module._io = mockXhr;
-    },
-
-    tearDown: function() {
-        module._io = this.old_io;
+        this.mockIO(mockXhr, module);
     },
 
     testSingletonCreation: function() {
@@ -55,27 +50,16 @@
 
 }));
 
-suite.add(new Y.Test.Case({
+suite.add(new Y.maas.testing.TestCase({
     name: 'test-add-node-widget-add-node',
 
-    mockIO: function(mock) {
-        this.old_io = module._io;
-        module._io = mock;
-    },
-
-    tearDown: function() {
-        if (Y.Lang.isValue(this.old_io)) {
-            module._io = this.old_io;
-        }
-    },
-
     testAddNodeAPICall: function() {
         var mockXhr = Y.Mock();
         Y.Mock.expect(mockXhr, {
             method: 'io',
             args: [MAAS_config.uris.nodes_handler, Y.Mock.Value.Any]
         });
-        this.mockIO(mockXhr);
+        this.mockIO(mockXhr, module);
         module.showAddNodeWidget();
         var overlay = module._add_node_singleton;
         overlay.get('srcNode').one('#id_hostname').set('value', 'host');
@@ -87,28 +71,23 @@
     testNodeidPopulation: function() {
         var mockXhr = new Y.Base();
         mockXhr.io = function(url, cfg) {
-            cfg.on.success(
-               3,
-               {response: Y.JSON.stringify({system_id: 3})});
+            cfg.on.success(3, {response: Y.JSON.stringify({system_id: 3})});
         };
-        this.mockIO(mockXhr);
+        this.mockIO(mockXhr, module);
         module.showAddNodeWidget();
         var overlay = module._add_node_singleton;
         overlay.get('srcNode').one('#id_hostname').set('value', 'host');
         var button = overlay.get('srcNode').one('button');
 
         var fired = false;
-        var handle = module.AddNodeDispatcher.on(
-            module.NODE_ADDED_EVENT, function(e, node){
-            Y.Assert.areEqual(3, node.system_id);
-            fired = true;
-        });
-        try {
-            button.simulate('click');
-        }
-        finally {
-            handle.detach();
-        }
+        this.registerListener(
+            Y.maas.node_add.AddNodeDispatcher, module.NODE_ADDED_EVENT,
+            function(e, node){
+                Y.Assert.areEqual(3, node.system_id);
+                fired = true;
+            }
+        );
+        button.simulate('click');
         Y.Assert.isTrue(fired);
     }
 
@@ -117,5 +96,5 @@
 namespace.suite = suite;
 
 }, '0.1', {'requires': [
-    'node-event-simulate', 'test', 'maas.add_node']}
+    'node-event-simulate', 'test', 'maas.testing', 'maas.node_add']}
 );

=== added file 'src/maasserver/static/js/tests/test_node_views.html'
--- src/maasserver/static/js/tests/test_node_views.html	1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/tests/test_node_views.html	2012-02-03 14:44:20 +0000
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";>
+<html xmlns="http://www.w3.org/1999/xhtml"; xml:lang="en" lang="en">
+  <head>
+    <title>Test maas.node_views</title>
+
+    <!-- YUI and test setup -->
+    <script type="text/javascript" src="../yui/tests/yui/yui-min.js"></script>
+    <script type="text/javascript" src="../testing/testrunner.js"></script>
+    <script type="text/javascript" src="../testing/testing.js"></script>
+    <script type="text/javascript" src="../node.js"></script>
+    <script type="text/javascript" src="../node_add.js"></script>
+    <!-- The module under test -->
+    <script type="text/javascript" src="../node_views.js"></script>
+    <!-- The test suite -->
+    <script type="text/javascript" src="test_node_views.js"></script>
+    <script type="text/javascript">
+    <!--
+    var MAAS_config = {
+      uris: {
+        statics: '/static/',
+        nodes_handler: '/api/nodes/'
+      }
+    };
+    // -->
+    </script>
+  </head>
+  <body>
+  <span id="suite">maas.node_views.tests</span>
+  <div id="placeholder"></div>
+  </body>
+</html>

=== added file 'src/maasserver/static/js/tests/test_node_views.js'
--- src/maasserver/static/js/tests/test_node_views.js	1970-01-01 00:00:00 +0000
+++ src/maasserver/static/js/tests/test_node_views.js	2012-02-03 14:44:20 +0000
@@ -0,0 +1,111 @@
+/* Copyright 2012 Canonical Ltd.  This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ */
+
+YUI({ useBrowserConsole: true }).add('maas.node_views.tests', function(Y) {
+
+Y.log('loading mass.node_views.tests');
+var namespace = Y.namespace('maas.node_views.tests');
+
+var module = Y.maas.node_views;
+var suite = new Y.Test.Suite("maas.node_views Tests");
+
+TestCase = Y.Base.create('viewDestroyer', Y.maas.testing.TestCase, [], {
+
+    tearDown: function() {
+        if (Y.Lang.isValue(this.view)) {
+            this.view.destroy();
+        }
+    }
+});
+
+suite.add(new TestCase({
+    name: 'test-node-views-NodeListLoader',
+
+    testInitialization: function() {
+        var base_view = new Y.maas.node_views.NodeListLoader();
+        Y.Assert.areEqual('nodeList', base_view.modelList.name);
+        Y.Assert.isFalse(base_view.nodes_loaded);
+    },
+
+    testRenderCallsLoad: function() {
+        // The initial call to .render() triggers the loading of the
+        // nodes.
+        var mockXhr = Y.Mock();
+        Y.Mock.expect(mockXhr, {
+            method: 'io',
+            args: [MAAS_config.uris.nodes_handler, Y.Mock.Value.Any]
+        });
+        this.mockIO(mockXhr, module);
+
+        var base_view = new Y.maas.node_views.NodeListLoader();
+        base_view.render();
+        Y.Mock.verify(mockXhr);
+    },
+
+    testDispatcherRegistered: function() {
+        // The view listens to Y.maas.node_add.AddNodeDispatcher and
+        // adds the published nodes to its internal this.modelList.
+        var base_view = new Y.maas.node_views.NodeListLoader();
+        Y.maas.node_add.AddNodeDispatcher.fire(
+            Y.maas.node_add.NODE_ADDED_EVENT, {},
+            {system_id: '4', hostname: 'dan'});
+        Y.Assert.areEqual(1, base_view.modelList.size());
+        Y.Assert.areEqual('dan', base_view.modelList.item(0).get('hostname'));
+    },
+
+    testLoadNodes: function() {
+        var response = Y.JSON.stringify([
+               {system_id: '3', hostname: 'dan'},
+               {system_id: '4', hostname: 'dee'}
+           ]);
+        this.mockSuccess(response, module);
+        var base_view = new Y.maas.node_views.NodeListLoader();
+        base_view.render();
+        Y.Assert.areEqual(2, base_view.modelList.size());
+        Y.Assert.areEqual('dan', base_view.modelList.item(0).get('hostname'));
+        Y.Assert.areEqual('dee', base_view.modelList.item(1).get('hostname'));
+    }
+
+}));
+
+suite.add(new TestCase({
+    name: 'test-node-views-NodeDashBoard',
+
+    testDisplay: function() {
+        var response = Y.JSON.stringify([
+            {system_id: '3', hostname: 'dan'},
+            {system_id: '4', hostname: 'dee'}
+        ]);
+        this.mockSuccess(response, module);
+        this.view = new Y.maas.node_views.NodesDashboard(
+            {append: '#placeholder'});
+        this.view.render();
+        Y.Assert.areEqual(
+            '2 nodes in this cluster',
+            Y.one('#placeholder').get('text'));
+    },
+
+    testDisplayUpdate: function() {
+        // The display is updated when new nodes are added.
+        this.mockSuccess(Y.JSON.stringify([]), module);
+        this.view = new Y.maas.node_views.NodesDashboard(
+            {append: '#placeholder'});
+        this.view.render();
+        Y.maas.node_add.AddNodeDispatcher.fire(
+            Y.maas.node_add.NODE_ADDED_EVENT, {},
+            {system_id: '4', hostname: 'dan'});
+        Y.Assert.areEqual(1, this.view.modelList.size());
+        Y.Assert.areEqual(
+            '1 node in this cluster',
+            Y.one('#placeholder').get('text'));
+    }
+
+}));
+
+
+namespace.suite = suite;
+
+}, '0.1', {'requires': [
+    'node-event-simulate', 'test', 'maas.testing', 'maas.node_views']}
+);

=== modified file 'src/maasserver/templates/maasserver/index.html'
--- src/maasserver/templates/maasserver/index.html	2012-02-02 09:06:18 +0000
+++ src/maasserver/templates/maasserver/index.html	2012-02-03 14:44:20 +0000
@@ -8,9 +8,24 @@
 {% block head %}
   <script type="text/javascript">
   <!--
-  YUI().use('maas.add_node', function (Y) {
+  YUI().use('maas.node_add', 'maas.node','maas.node_views', function (Y) {
     Y.on('load', function() {
-      Y.one('#addnode').on('click', Y.maas.add_node.showAddNodeWidget);
+      // Create Dashboard view.
+      var view_container = Y.Node.create('<div />')
+          .set('id', 'dashboard');
+      Y.one('#content').append(view_container);
+      var view = new Y.maas.node_views.NodesDashboard(
+          {'append': '#dashboard'});
+      view.render(view_container);
+      // Create 'Add Node' link.
+      var add_node_link = Y.Node.create('<a />')
+          .set('id', 'addnode')
+          .set('text', "Add Node")
+          .set('href', '#');
+      Y.one('#content').append(add_node_link);
+      add_node_link.on('click', Y.maas.node_add.showAddNodeWidget);
+      // Wire up the view to the 'nodeAdded' event.
+      Y.one('#addnode').on('click', Y.maas.node_add.showAddNodeWidget);
     });
   });
   // -->
@@ -18,9 +33,4 @@
 {% endblock %}
 
 {% block content %}
-  <h2>
-  {{ node_list|length }} node{{ node_list|length|pluralize }} in this cluster
-  </h2>
-
-  <a id="addnode" href="#">Add Node</a>
 {% endblock %}

=== modified file 'src/maasserver/templates/maasserver/js-conf.html'
--- src/maasserver/templates/maasserver/js-conf.html	2012-02-02 09:19:28 +0000
+++ src/maasserver/templates/maasserver/js-conf.html	2012-02-03 14:44:20 +0000
@@ -17,5 +17,6 @@
 <script
   src="{{ STATIC_URL }}js/yui/{{ YUI_VERSION }}/yui-base/yui-base-min.js">
 </script>
-<script src="{{ STATIC_URL }}js/add_node.js">
-</script>
+<script src="{{ STATIC_URL }}js/node_add.js"></script>
+<script src="{{ STATIC_URL }}js/node.js"></script>
+<script src="{{ STATIC_URL }}js/node_views.js"></script>


Follow ups