← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~huwshimi/maas/dashboard-events into lp:maas

 

Huw Wilkins has proposed merging lp:~huwshimi/maas/dashboard-events into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~huwshimi/maas/dashboard-events/+merge/98576

This branch ties all the dashboard stuff together, including, animated chart, updating from events, hovers, design etc.

The tests are horrible, but hopefully they should test enough for now.


-- 
https://code.launchpad.net/~huwshimi/maas/dashboard-events/+merge/98576
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~huwshimi/maas/dashboard-events into lp:maas.
=== modified file 'src/maasserver/static/css/modifiers.css'
--- src/maasserver/static/css/modifiers.css	2012-03-05 05:52:32 +0000
+++ src/maasserver/static/css/modifiers.css	2012-03-21 05:48:06 +0000
@@ -22,3 +22,22 @@
     vertical-align: text-bottom;
     margin-right: 3px;
     }
+/* Spacing */
+.space-top {
+    margin-top: 20px;
+    }
+.space-top-none {
+    margin-top: 0;
+    }
+.space-bottom-none {
+    margin-bottom: 0;
+    }
+.space-bottom-small {
+    margin-bottom: 5px;
+    }
+.pad-top {
+    margin-top: 20px;
+    }
+.pad-top-large {
+    margin-top: 40px;
+    }

=== modified file 'src/maasserver/static/css/typography.css'
--- src/maasserver/static/css/typography.css	2012-02-09 00:42:27 +0000
+++ src/maasserver/static/css/typography.css	2012-03-21 05:48:06 +0000
@@ -17,6 +17,10 @@
     font-size: 22px;
     line-height: 26px;
     }
+h2.super-size {
+    font-size: 120px;
+    line-height: 100px;
+    }
 h3 {
     margin-top: 16px;
     margin-bottom: 8px;
@@ -27,6 +31,15 @@
     width: auto;
     margin-bottom: 0.8em;
     }
+p.large {
+    font-size: 24px;
+    }
+p.medium {
+    font-size: 16px;
+    }
+p.secondary {
+    color: #AEA79F;
+    }
 pre, code, samp, tt, .console {
     font-family: 'Ubuntu Mono', monospace;
     margin-bottom: 0.8em;

=== modified file 'src/maasserver/static/js/node_views.js'
--- src/maasserver/static/js/node_views.js	2012-03-15 13:58:32 +0000
+++ src/maasserver/static/js/node_views.js	2012-03-21 05:48:06 +0000
@@ -144,45 +144,264 @@
  */
 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" />'),
+    all_template: ('node{plural} in this MAAS'),
+    deployed_template: ('node{plural} deployed'),
+    commissioned_template: ('node{plural} commissioned'),
+    queued_template: ('node{plural} queued'),
+    offline_template: ('node{plural} offline'),
+    added_template: ('node{plural} added but never seen'),
+    reserved_template:
+        ('{nodes} node{plural} running without a registered service.'),
+    retired_template: ('{nodes} retired node{plural} not represented.'),
 
     initializer: function(config) {
-        this.append = config.append;
-       // Prepare spinnerNode.
+        this.srcNode = config.srcNode;
+        this.summaryNode = Y.one(config.summaryNode);
+        this.numberNode = Y.one(config.numberNode);
+        this.descriptionNode = Y.one(config.descriptionNode);
+        this.reservedNode = Y.one(config.reservedNode);
+        this.retiredNode = Y.one(config.retiredNode);
+        this.deployed_nodes = 0;
+        this.commissioned_nodes = 0;
+        this.queued_nodes = 0;
+        this.reserved_nodes = 0;
+        this.offline_nodes = 0;
+        this.added_nodes = 0;
+        this.retired_nodes = 0;
+        this.fade_out = new Y.Anim({
+            node: this.summaryNode,
+            to: {opacity: 0},
+            duration: 0.1,
+            easing: 'easeIn'
+            });
+        this.fade_in = new Y.Anim({
+            node: this.summaryNode,
+            to: {opacity: 1},
+            duration: 0.2,
+            easing: 'easeIn'
+            });
+        // Prepare spinnerNode.
         this.spinnerNode = Y.Node.create('<img />')
             .set('src', MAAS_config.uris.statics + 'img/spinner.gif');
+        // Set up the chart
+        this.chart = new Y.maas.nodes_chart.NodesChartWidget({
+            node_id: 'chart',
+            width: 300
+            });
+
+        // Set up the event listeners for node changes
+        Y.on('Node.updated', function(e, widget) {
+            widget.updateNode('updated', e.instance);
+        }, null, this);
+
+        Y.on('Node.created', function(e, widget) {
+            widget.updateNode('created', e.instance);
+        }, null, this);
+
+        Y.on('Node.deleted', function(e, widget) {
+            widget.updateNode('deleted', e.instance);
+        }, null, this);
+
+        // Set up the hovers for changing the dashboard text
+        var events = [
+            {event: 'hover.offline.over', template: this.offline_template},
+            {event: 'hover.offline.out'},
+            {event: 'hover.added.over', template: this.added_template},
+            {event: 'hover.added.out'},
+            {event: 'hover.deployed.over', template: this.deployed_template},
+            {event: 'hover.deployed.out'},
+            {
+                event: 'hover.commissioned.over',
+                template: this.commissioned_template
+                },
+            {event: 'hover.commissioned.out'},
+            {event: 'hover.queued.over', template: this.queued_template},
+            {event: 'hover.queued.out'}
+            ];
+        for (var ev in events) {
+            this.chart.on(events[ev].event, function(e, template, widget) {
+                if (Y.Lang.isValue(e.nodes)) {
+                    widget.setSummary(true, e.nodes, template, true);
+                }
+                else {
+                    // Set the text to the default
+                    widget.setSummary(true);
+                }
+            }, null, events[ev].template, this);
+        }
     },
 
    /**
-    * Display a dashboard of the nodes (right now a simple count).
+    * Display a dashboard of the nodes.
     *
     * @method display
     */
     display: function () {
-        var size = this.modelList.size();
-        var template = (size === 1) ?
-            this.singular_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);
+        /* Set up the initial node/status counts. This needs to happen here
+           so that this.modelList exists.
+        */
+        if (!Y.Lang.isValue(this.nodes)) {
+            this.nodes = {};
+            for (var i=0; i<this.modelList.size(); i++) {
+                var node = this.modelList.item(i);
+                var status = node.get('status');
+                this.updateStatus('add', status);
+                this.nodes[node.get('system_id')] = node.get('status');
+            }
         }
+        // Update the chart with the new node/status counts
+        this.chart.updateChart();
+        // Set the default text on the dashboard
+        this.setSummary(false);
+        this.setNodeText(
+            this.reservedNode, this.reserved_template, this.reserved_nodes);
+        this.setNodeText(
+            this.retiredNode, this.retired_template, this.retired_nodes);
     },
 
     loadNodesStarted: function() {
-        Y.one(this.append).insert(this.spinnerNode, 0);
+        Y.one(this.srcNode).insert(this.spinnerNode, 0);
     },
 
     loadNodesEnded: function() {
         this.spinnerNode.remove();
+    },
+
+   /**
+    * Update the nodes in the chart.
+    */
+    updateNode: function(action, node) {
+        var update_chart = false;
+        if (action == 'created') {
+            this.nodes[node.system_id] = node.status;
+            update_chart = this.updateStatus('add', node.status);
+        }
+        else if (action == 'deleted') {
+            delete this.nodes[node.system_id];
+            update_chart = this.updateStatus('remove', node.status);
+        }
+        else if (action == 'updated') {
+            previous_status = this.nodes[node.system_id];
+            this.nodes[node.system_id] = node.status;
+            update_remove = this.updateStatus('remove', previous_status);
+            update_add = this.updateStatus('add', node.status);
+            if (update_remove || update_add) {
+                update_chart = true;
+            }
+        }
+
+        if (update_chart) {
+            // Update the chart with the new node/status counts
+            this.chart.updateChart();
+        }
+
+        if (action != 'updated') {
+            /* Set the default text on the dashboard. We only need to do this
+               if the total number of nodes has changed.
+            */
+            this.setSummary(true);
+        }
+    },
+
+   /**
+    * Update the number of nodes for a status.
+    */
+    updateStatus: function(action, status) {
+        var update_chart = false;
+        /* This seems like an ugly way to calculate the change, but it stops
+           duplication of checking for the action for each status.
+        */
+        if (action == 'add') {
+            var node_counter = 1;
+        }
+        else if (action == 'remove') {
+            var node_counter = -1;
+        }
+
+        /* TODO: The commissioned status currently doesn't exist, but once it
+           does it should be added here too.
+        */
+        if (status == 0) {
+            // Added nodes
+            this.added_nodes += node_counter;
+            this.chart.set('added_nodes', this.added_nodes);
+            update_chart = true;
+        }
+        else if (status == 1 || status == 2 || status == 3) {
+            // Offline nodes
+            this.offline_nodes += node_counter;
+            this.chart.set('offline_nodes', this.offline_nodes);
+            update_chart = true;
+        }
+        else if (status == 4) {
+            // Queued nodes
+            this.queued_nodes += node_counter;
+            this.chart.set('queued_nodes', this.queued_nodes);
+            update_chart = true;
+        }
+        else if (status == 5) {
+            // Reserved nodes
+            this.reserved_nodes += node_counter;
+            this.setNodeText(
+                this.reservedNode,
+                this.reserved_template,
+                this.reserved_nodes
+                );
+        }
+        else if (status == 6) {
+            // Deployed nodes
+            this.deployed_nodes += node_counter;
+            this.chart.set('deployed_nodes', this.deployed_nodes);
+            update_chart = true;
+        }
+        else if (status == 7) {
+            // Retired nodes
+            this.retired_nodes += node_counter;
+            this.setNodeText(
+                this.retiredNode, this.retired_template, this.retired_nodes);
+        }
+
+        return update_chart;
+    },
+
+   /**
+    * Set the text for the number of nodes for a status.
+    */
+    setSummary: function(animate, nodes, template) {
+        // By default we just want to display the total nodes.
+        if (!nodes || !template) {
+            nodes = this.modelList.size();
+            template = this.all_template;
+        }
+        plural = (nodes === 1) ? '' : 's';
+        text = Y.Lang.sub(template, {plural: plural})
+
+        if (animate) {
+            this.fade_out.run();
+            this.fade_out.on('end', function (e, self, nodes, text) {
+                self.numberNode.setContent(nodes);
+                self.descriptionNode.setContent(text);
+                self.fade_in.run();
+            }, null, this, nodes, text);
+        }
+        else {
+            this.numberNode.setContent(nodes);
+            this.descriptionNode.setContent(text);
+        }
+    },
+
+   /**
+    * Set the text from a template for a DOM node.
+    */
+    setNodeText: function(element, template, nodes) {
+        plural = (nodes === 1) ? '' : 's';
+        text = Y.Lang.sub(template, {plural: plural, nodes: nodes})
+        element.setContent(text);
     }
 
 });
 
-}, '0.1', {'requires': ['view', 'io', 'maas.node', 'maas.node_add']}
+}, '0.1', {'requires': [
+    'view', 'io', 'maas.node', 'maas.node_add', 'maas.nodes_chart',
+    'maas.morph', 'anim']}
 );

=== modified file 'src/maasserver/static/js/nodes_chart.js'
--- src/maasserver/static/js/nodes_chart.js	2012-03-19 07:13:21 +0000
+++ src/maasserver/static/js/nodes_chart.js	2012-03-21 05:48:06 +0000
@@ -157,6 +157,8 @@
         var outer_nodes = [
             {
                 nodes: deployed_nodes,
+                name: 'deployed_nodes',
+                colour: OUTER_COLOURS[0],
                 events: {
                     over: 'hover.deployed.over',
                     out: 'hover.deployed.out'
@@ -164,6 +166,8 @@
                 },
             {
                 nodes: commissioned_nodes,
+                name: 'commissioned_nodes',
+                colour: OUTER_COLOURS[2],
                 events: {
                     over: 'hover.commissioned.over',
                     out: 'hover.commissioned.out'
@@ -171,6 +175,8 @@
                 },
             {
                 nodes: queued_nodes,
+                name: 'queued_nodes',
+                colour: OUTER_COLOURS[1],
                 events: {
                     over: 'hover.queued.over',
                     out: 'hover.queued.out'
@@ -203,24 +209,24 @@
                     var slice = r.path();
                     slice.attr({
                         segment: segment,
-                        fill: OUTER_COLOURS[i],
+                        fill: outer_nodes[i].colour,
                         stroke: STROKE_COLOUR,
                         'stroke-width': STROKE_WIDTH
                         });
                     Y.one(slice.node).on(
                         'hover',
+                        function(e, over, out, name, widget) {
+                            widget.fire(over, {nodes: widget.get(name)});
+                        },
                         function(e, over, out, nodes, widget) {
-        			        widget.fire(over, {nodes: nodes});
-		                },
-		                function(e, over, out, nodes, widget) {
-		                    widget.fire(out);
-		                },
-		                null,
-		                outer_nodes[i].events.over,
-		                outer_nodes[i].events.out,
-		                outer_nodes[i].nodes,
-		                this
-		                );
+                            widget.fire(out);
+                        },
+                        null,
+                        outer_nodes[i].events.over,
+                        outer_nodes[i].events.out,
+                        outer_nodes[i].name,
+                        this
+                        );
                     this._outer_paths.push(slice);
                 }
                 else {
@@ -243,14 +249,15 @@
                 Y.one(this._offline_circle[0].node).on(
                     'hover',
                     function(e, widget) {
-			            widget.fire(
-			                'hover.offline.over', {nodes: offline_nodes});
-		            },
-		            function(e, widget) {
-			            widget.fire('hover.offline.out');
-		            },
-		            null,
-		            this);
+                        widget.fire(
+                            'hover.offline.over',
+                            {nodes: widget.get('offline_nodes')});
+                    },
+                    function(e, widget) {
+                        widget.fire('hover.offline.out');
+                    },
+                    null,
+                    this);
             }
         }
         else {
@@ -272,13 +279,15 @@
             Y.one(this._added_circle[0].node).on(
                 'hover',
                 function(e, widget) {
-			        widget.fire('hover.added.over', {nodes: added_nodes});
-		        },
-		        function(e, widget) {
-			        widget.fire('hover.added.out');
-		        },
-		        null,
-		        this);
+                    widget.fire(
+                        'hover.added.over',
+                        {nodes: widget.get('added_nodes')});
+                },
+                function(e, widget) {
+                    widget.fire('hover.added.out');
+                },
+                null,
+                this);
         }
         else {
             if (added_nodes != total_nodes) {
@@ -293,19 +302,8 @@
     },
 
     initializer: function(cfg) {
-        /* Publish the hover events. */
-        this.publish('hover.offline.over');
-        this.publish('hover.offline.out');
-        this.publish('hover.added.over');
-        this.publish('hover.added.out');
-        this.publish('hover.deployed.over');
-        this.publish('hover.deployed.out');
-        this.publish('hover.commissioned.over');
-        this.publish('hover.commissioned.out');
-        this.publish('hover.queued.over');
-        this.publish('hover.queued.out');
-
-        r = Raphael(this.get('node_id'));
+        canvas_size = this.get('width') + STROKE_WIDTH * 2;
+        r = Raphael(this.get('node_id'), canvas_size, canvas_size);
         r.customAttributes.segment = function (x, y, r, a1, a2) {
             var flag = (a2 - a1) > 180;
             if (a1 == 0 && a2 == 360) {

=== modified file 'src/maasserver/static/js/tests/test_node_views.html'
--- src/maasserver/static/js/tests/test_node_views.html	2012-03-15 13:58:32 +0000
+++ src/maasserver/static/js/tests/test_node_views.html	2012-03-21 05:48:06 +0000
@@ -4,12 +4,14 @@
     <title>Test maas.node_views</title>
 
     <!-- YUI and test setup -->
+    <script type="text/javascript" src="../../jslibs/raphael/raphael-min.js"></script>
     <script type="text/javascript" src="../testing/yui_test_conf.js"></script>
     <script type="text/javascript" src="../../jslibs/yui/tests/build/yui/yui.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>
+    <script type="text/javascript" src="../nodes_chart.js"></script>
     <!-- The module under test -->
     <script type="text/javascript" src="../node_views.js"></script>
     <!-- The test suite -->
@@ -27,6 +29,14 @@
   </head>
   <body>
   <span id="suite">maas.node_views.tests</span>
-  <div id="placeholder"></div>
+  <div id="dashboard">
+    <div id="chart"></div>
+    <div id="summary">
+      <h2 id="nodes-number"></h2>
+      <p id="nodes-description"></p>
+    </div>
+    <p id="reserved-nodes"></p>
+    <p id="retired-nodes"></p>
+  </div>
   </body>
 </html>

=== modified file 'src/maasserver/static/js/tests/test_node_views.js'
--- src/maasserver/static/js/tests/test_node_views.js	2012-03-15 13:58:32 +0000
+++ src/maasserver/static/js/tests/test_node_views.js	2012-03-21 05:48:06 +0000
@@ -66,39 +66,349 @@
 suite.add(new Y.maas.testing.TestCase({
     name: 'test-node-views-NodeDashBoard',
 
+    setUp : function () {
+        this.data = [
+            {system_id: 'sys1', hostname: 'host1', status: 0},
+            {system_id: 'sys2', hostname: 'host2', status: 0},
+            {system_id: 'sys3', hostname: 'host3', status: 1},
+            {system_id: 'sys4', hostname: 'host4', status: 2},
+            {system_id: 'sys5', hostname: 'host5', status: 2},
+            {system_id: 'sys6', hostname: 'host6', status: 3},
+            {system_id: 'sys7', hostname: 'host7', status: 4},
+            {system_id: 'sys8', hostname: 'host8', status: 4},
+            {system_id: 'sys9', hostname: 'host9', status: 5},
+            {system_id: 'sys10', hostname: 'host10', status: 5},
+            {system_id: 'sys11', hostname: 'host11', status: 5},
+            {system_id: 'sys12', hostname: 'host12', status: 6},
+            {system_id: 'sys13', hostname: 'host13', status: 7}
+        ];
+    },
+
+    testInitializer: function() {
+        var view = create_dashboard_view(this.data, this);
+        this.addCleanup(function() { view.destroy(); });
+        view.render();
+        Y.Assert.areNotEqual(
+            '',
+            Y.one('#chart').get('text'),
+            'The chart node should have been populated');
+
+        // Chart hovers should be set up
+        Y.one(view.chart._offline_circle[0].node).simulate('mouseover');
+        this.wait(function() {
+            Y.Assert.areEqual(
+                '4',
+                Y.one('#nodes-number').get('text'),
+                'The total number of offline nodes should be set');
+            Y.Assert.areEqual(
+                'nodes offline',
+                Y.one('#nodes-description').get('text'),
+                'The text should be set with nodes as a plural');
+        }, 500);
+
+        Y.one(view.chart._offline_circle[0].node).simulate('mouseout');
+        this.wait(function() {
+            Y.Assert.areEqual(
+                '13',
+                Y.one('#nodes-number').get('text'),
+                'The total number of nodes should be set');
+            Y.Assert.areEqual(
+                'nodes in this MAAS',
+                Y.one('#nodes-description').get('text'),
+                'The default text should be set');
+        }, 500);
+    },
+
     testDisplay: function() {
-        var response = Y.JSON.stringify([
-            {system_id: '3', hostname: 'dan'},
-            {system_id: '4', hostname: 'dee'}
-        ]);
-        this.mockSuccess(response, module);
-        var view = new Y.maas.node_views.NodesDashboard(
-            {append: '#placeholder'});
+        var view = create_dashboard_view(this.data, this);
         this.addCleanup(function() { view.destroy(); });
         view.render();
-        Y.Assert.areEqual(
-            '2 nodes in this cluster',
-            Y.one('#placeholder').get('text'));
+        for (var node in this.data){
+            Y.Assert.areEqual(
+                this.data[node].status,
+                view.nodes[this.data[node].system_id],
+                'The list of nodes should have been populated');
+        }
+        Y.Assert.areEqual(
+            '13',
+            Y.one('#nodes-number').get('text'),
+            'The total number of nodes should be set');
+        Y.Assert.areEqual(
+            'nodes in this MAAS',
+            Y.one('#nodes-description').get('text'),
+            'The summary text should be set');
+        Y.Assert.areEqual(
+            '3 nodes running without a registered service.',
+            Y.one('#reserved-nodes').get('text'),
+            'The reserved text should be set');
+        Y.Assert.areEqual(
+            '1 retired node not represented.',
+            Y.one('#retired-nodes').get('text'),
+            'The retired text should be set');
     },
 
-    testDisplayUpdate: function() {
-        // The display is updated when new nodes are added.
-        this.mockSuccess(Y.JSON.stringify([]), module);
-        var view = new Y.maas.node_views.NodesDashboard(
-            {append: '#placeholder'});
+    testUpdateNode: function() {
+        /* TODO: check updateNode is fired by Node.created, Node.updated
+           and Node.deleted.
+        */
+        var view = create_dashboard_view(this.data, this);
+        var node = {system_id: 'sys14', hostname: 'host14', status: 0};
         this.addCleanup(function() { view.destroy(); });
         view.render();
+        Y.Assert.areEqual(
+            '13',
+            Y.one('#nodes-number').get('text'),
+            'The total number of nodes should be set');
+        // Check node creation
+        Y.Assert.areEqual(
+            2,
+            view.added_nodes,
+            'Check the initial number of nodes for the status');
         Y.maas.node_add.AddNodeDispatcher.fire(
-            Y.maas.node_add.NODE_ADDED_EVENT, {},
-            {system_id: '4', hostname: 'dan'});
-        Y.Assert.areEqual(1, view.modelList.size());
-        Y.Assert.areEqual(
-            '1 node in this cluster',
-            Y.one('#placeholder').get('text'));
+            Y.maas.node_add.NODE_ADDED_EVENT, {}, node);
+        view.updateNode('created', node);
+        Y.Assert.areEqual(
+            0,
+            view.nodes['sys14'],
+            'The node and status should be recorded');
+        Y.Assert.areEqual(
+            3,
+            view.added_nodes,
+            'The status should have one extra node');
+        Y.Assert.areEqual(
+            3,
+            view.chart.get('added_nodes'),
+            'The chart status number should also be updated');
+        this.wait(function() {
+            Y.Assert.areEqual(
+                '14',
+                Y.one('#nodes-number').get('text'),
+                'The total number of nodes should have been updated');
+        }, 500);
+        // Check node updating
+        node.status = 6;
+        Y.Assert.areEqual(
+            1,
+            view.deployed_nodes,
+            'Check the initial number of nodes for the new status');
+        view.updateNode('updated', node);
+        Y.Assert.areEqual(
+            6,
+            view.nodes['sys14'],
+            'The node status should have been updated');
+        Y.Assert.areEqual(
+            2,
+            view.deployed_nodes,
+            'The new status should have one extra node');
+        Y.Assert.areEqual(
+            2,
+            view.chart.get('deployed_nodes'),
+            'The new chart status number should also be updated');
+        Y.Assert.areEqual(
+            2,
+            view.added_nodes,
+            'The old status should have one less node');
+        Y.Assert.areEqual(
+            2,
+            view.chart.get('added_nodes'),
+            'The old chart status number should also be updated');
+        this.wait(function() {
+            Y.Assert.areEqual(
+                Y.one('#nodes-number').get('text'),
+                '14',
+                'The total number of nodes should not have been updated');
+        }, 500);
+
+        // Check node deleting
+        view.updateNode('deleted', node);
+        Y.Assert.isUndefined(
+            view.nodes['sys14'],
+            'The node status should have been deleted');
+        Y.Assert.areEqual(
+            1,
+            view.deployed_nodes,
+            'The status should have one less node');
+        Y.Assert.areEqual(
+            1,
+            view.chart.get('deployed_nodes'),
+            'The chart status number should also be updated');
+        this.wait(function() {
+            Y.Assert.areEqual(
+                '13',
+                Y.one('#nodes-number').get('text'),
+                'The total number of nodes should have been updated');
+        }, 500);
+    },
+
+    testUpdateStatus: function() {
+        var view = create_dashboard_view(this.data, this);
+        this.addCleanup(function() { view.destroy(); });
+        view.render();
+        // Add a node to a status that also updates the chart
+        Y.Assert.areEqual(
+            2,
+            view.added_nodes,
+            'Check the initial number of nodes for the status');
+        var result = view.updateStatus('add', 0);
+        Y.Assert.areEqual(
+            3,
+            view.added_nodes,
+            'The status should have one extra node');
+        Y.Assert.areEqual(
+            3,
+            view.chart.get('added_nodes'),
+            'The chart status number should also be updated');
+        Y.Assert.isTrue(
+            result,
+            'This status needs to update the chart, so it should return true');
+        // Remove a node from a status
+        result = view.updateStatus('remove', 0);
+        Y.Assert.areEqual(
+            2,
+            view.added_nodes,
+            'The status should have one less node');
+        Y.Assert.areEqual(
+            2,
+            view.chart.get('added_nodes'),
+            'The chart status number should also be updated');
+        // Check a status that also updates text
+        Y.Assert.areEqual(
+            3,
+            view.reserved_nodes,
+            'Check the initial number of nodes for the reserved status');
+        result = view.updateStatus('add', 5);
+        Y.Assert.areEqual(
+            4,
+            view.reserved_nodes,
+            'The status should have one extra node');
+        Y.Assert.areEqual(
+            '4 nodes running without a registered service.',
+            Y.one('#reserved-nodes').get('text'),
+            'The dashboard reserved text should be updated');
+        Y.Assert.isFalse(
+            result,
+            'This status should not to update the chart');
+    },
+
+    testSetSummary: function() {
+        // Test the default summary, with more than one node
+        var data = [
+            {system_id: 'sys9', hostname: 'host9', status: 5}
+        ];
+        var view = create_dashboard_view(data, this);
+        this.addCleanup(function() { view.destroy(); });
+        view.render();
+        view.setSummary(false);
+        Y.Assert.areEqual(
+            '1',
+            Y.one('#nodes-number').get('text'),
+            'The total number of nodes should be set');
+        Y.Assert.areEqual(
+            'node in this MAAS',
+            Y.one('#nodes-description').get('text'),
+            'The text should be set with nodes as singular');
+
+        // Test the default summary, with one node
+        view = create_dashboard_view(this.data, this);
+        view.render();
+        view.setSummary(false);
+        Y.Assert.areEqual(
+            '13',
+            Y.one('#nodes-number').get('text'),
+            'The total number of nodes should be set');
+        Y.Assert.areEqual(
+            'nodes in this MAAS',
+            Y.one('#nodes-description').get('text'),
+            'The text should be set with nodes as a plural');
+
+        // Test the animation runs if we want it too
+        var fade_out_anim = false;
+        var fade_in_anim = false;
+        view.fade_out.on('end', function() {
+            fade_out_anim = true;
+        });
+        view.fade_in.on('end', function() {
+            fade_in_anim = true;
+        });
+        view.setSummary(true);
+        this.wait(function() {
+            Y.Assert.isTrue(
+                fade_out_anim,
+                'The fade out animation should have run');
+            Y.Assert.isTrue(
+                fade_in_anim,
+                'The fade in animation should have run');
+        }, 500);
+
+        // Test the animation doesn't run if we don't want it too
+        fade_out_anim = false;
+        fade_in_anim = false;
+        view.setSummary(false);
+        this.wait(function() {
+            Y.Assert.isFalse(
+                fade_out_anim,
+                'The fade out animation should not have run');
+            Y.Assert.isFalse(
+                fade_in_anim,
+                'The fade in animation should not have run');
+        }, 500);
+
+        // Test we can set the summary for a particular status (multiple nodes)
+        view = create_dashboard_view(this.data, this);
+        view.render();
+        view.setSummary(false, 1, view.queued_template);
+        Y.Assert.areEqual(
+            '1',
+            Y.one('#nodes-number').get('text'),
+            'The total number of nodes should be set');
+        Y.Assert.areEqual(
+            'node queued',
+            Y.one('#nodes-description').get('text'),
+            'The text should be set with nodes as a plural');
+    },
+
+    testSetNodeText: function() {
+        var view = create_dashboard_view(this.data, this);
+        this.addCleanup(function() { view.destroy(); });
+        view.render();
+        view.setNodeText(
+            view.reservedNode, view.reserved_template, view.reserved_nodes);
+        Y.Assert.areEqual(
+            '3 nodes running without a registered service.',
+            Y.one('#reserved-nodes').get('text'),
+            'The text should be set with nodes as a plural');
+
+        var data = [
+            {system_id: 'sys9', hostname: 'host9', status: 5}
+        ];
+        view = create_dashboard_view(data, this);
+        view.render();
+        view.setNodeText(
+            view.reservedNode, view.reserved_template, view.reserved_nodes);
+        Y.Assert.areEqual(
+            '1 node running without a registered service.',
+            Y.one('#reserved-nodes').get('text'),
+            'The text should be set with nodes as singular');
+    },
+
+    tearDown : function () {
+        Y.one('#chart').set('text', '');
     }
-
 }));
 
+function create_dashboard_view(data, self) {
+    var response = Y.JSON.stringify(data);
+    self.mockSuccess(response, module);
+    var view = new Y.maas.node_views.NodesDashboard({
+        srcNode: '#dashboard',
+        summaryNode: '#summary',
+        numberNode: '#nodes-number',
+        descriptionNode: '#nodes-description',
+        reservedNode: '#reserved-nodes',
+        retiredNode: '#retired-nodes'});
+    return view;
+}
+
 
 namespace.suite = suite;
 

=== modified file 'src/maasserver/templates/maasserver/index.html'
--- src/maasserver/templates/maasserver/index.html	2012-03-19 03:24:16 +0000
+++ src/maasserver/templates/maasserver/index.html	2012-03-21 05:48:06 +0000
@@ -13,52 +13,26 @@
   <script type="text/javascript">
   <!--
   YUI().use(
-    'maas.node_add', 'maas.node','maas.node_views', 'maas.utils', 'maas.nodes_chart',
+    'maas.node_add', 'maas.node','maas.node_views', 'maas.utils',
     'maas.longpoll', 
     function (Y) {
     Y.on('load', function() {
       // 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', '#')
-          .addClass('button');
-      Y.one('#content').append(add_node_link);
-      add_node_link.on('click', Y.maas.node_add.showAddNodeWidget);
+      var view = new Y.maas.node_views.NodesDashboard({
+          srcNode: '#dashboard',
+          summaryNode: '#summary',
+          numberNode: '#nodes-number',
+          descriptionNode: '#nodes-description',
+          reservedNode: '#reserved-nodes',
+          retiredNode: '#retired-nodes'});
+      view.render();
+      Y.one('#addnode').on('click', Y.maas.node_add.showAddNodeWidget);
 
       // Setup TitleEditWidget.
       var title_widget = new Y.maas.utils.TitleEditWidget(
 	  {srcNode: '.page-title-form'});
       title_widget.render();
 
-
-      // Show the chart
-      var cfg = {
-        node_id: 'chart',
-        width: 300
-        };
-      var chart = new Y.maas.nodes_chart.NodesChartWidget(cfg);
-
-      // Sample event listeners.
-      Y.on("Node.updated", function() {
-		  Y.log("node updated");
-      });
-
-      Y.on("Node.created", function() {
-		  Y.log("node created");
-      });
-
-      Y.on("Node.deleted", function() {
-		  Y.log("node deleted");
-      });
-
       // Start longpoll.
       {% if longpoll_queue and LONGPOLL_PATH %}
         Y.later(0, Y.maas.longpoll, function() {
@@ -85,5 +59,16 @@
 {% endblock %}
 
 {% block content %}
-    <div id="chart"></div>
+  <div id="dashboard" class="pad-top">
+    <div id="chart" class="block size6"></div>
+    <div class="block block size8">
+      <div id="summary">
+        <h2 id="nodes-number" class="super-size pad-top-large"></h2>
+        <p id="nodes-description" class="large"></p>
+      </div>
+      <p id="reserved-nodes" class="medium space-bottom-small"></p>
+      <p id="retired-nodes" class="secondary medium space-top-none"></p>
+      <a href="#" id="addnode" class="button right space-top">Add node</a>
+    </div>
+  </div>
 {% endblock %}


Follow ups