Updated Nodes Overview page
Displays node counts, mocked Health Status table, defined template structure to resemble wireframes Change-Id: Ia042bf414d48eaf0254df92db2b1557b9ab63ea3
This commit is contained in:
		@@ -66,10 +66,10 @@ class FreeNodesTable(NodesTable):
 | 
			
		||||
        row_actions = ()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ResourceNodesTable(NodesTable):
 | 
			
		||||
class DeployedNodesTable(NodesTable):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        name = "resource_nodes"
 | 
			
		||||
        verbose_name = _("Resource Nodes")
 | 
			
		||||
        name = "deployed_nodes"
 | 
			
		||||
        verbose_name = _("Deployed Nodes")
 | 
			
		||||
        table_actions = ()
 | 
			
		||||
        row_actions = ()
 | 
			
		||||
 
 | 
			
		||||
@@ -31,41 +31,54 @@ class OverviewTab(tabs.Tab):
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, request):
 | 
			
		||||
        try:
 | 
			
		||||
            free_nodes = len(api.Node.list(request, associated=False))
 | 
			
		||||
            free_nodes = api.Node.list(request, associated=False)
 | 
			
		||||
            deployed_nodes = api.Node.list(request, associated=True)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            free_nodes = 0
 | 
			
		||||
            free_nodes = []
 | 
			
		||||
            deployed_nodes = []
 | 
			
		||||
            exceptions.handle(request,
 | 
			
		||||
                              _('Unable to retrieve free nodes.'))
 | 
			
		||||
        try:
 | 
			
		||||
            resource_nodes = len(api.Node.list(request, associated=True))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            resource_nodes = 0
 | 
			
		||||
            exceptions.handle(request,
 | 
			
		||||
                              _('Unable to retrieve resource nodes.'))
 | 
			
		||||
                              _('Unable to retrieve nodes.'))
 | 
			
		||||
 | 
			
		||||
        free_nodes_down = [node for node in free_nodes
 | 
			
		||||
                           if node.power_state != 'on']
 | 
			
		||||
        deployed_nodes_down = [node for node in deployed_nodes
 | 
			
		||||
                               if node.power_state != 'on']
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            'nodes_total': free_nodes + resource_nodes,
 | 
			
		||||
            'nodes_resources': resource_nodes,
 | 
			
		||||
            'nodes_free': free_nodes,
 | 
			
		||||
            'deployed_nodes': deployed_nodes,
 | 
			
		||||
            'deployed_nodes_down': deployed_nodes_down,
 | 
			
		||||
            'free_nodes': free_nodes,
 | 
			
		||||
            'free_nodes_down': free_nodes_down,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ResourceTab(tabs.TableTab):
 | 
			
		||||
    table_classes = (tables.ResourceNodesTable,)
 | 
			
		||||
    name = _("Resource")
 | 
			
		||||
    slug = "resource"
 | 
			
		||||
class DeployedTab(tabs.TableTab):
 | 
			
		||||
    table_classes = (tables.DeployedNodesTable,)
 | 
			
		||||
    name = _("Deployed")
 | 
			
		||||
    slug = "deployed"
 | 
			
		||||
    template_name = ("horizon/common/_detail_table.html")
 | 
			
		||||
 | 
			
		||||
    def get_resource_nodes_data(self):
 | 
			
		||||
    def get_items_count(self):
 | 
			
		||||
        try:
 | 
			
		||||
            resource_nodes = api.Node.list(self.request, associated=True)
 | 
			
		||||
            deployed_nodes_count = len(api.Node.list(self.request,
 | 
			
		||||
                                                     associated=True))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            resource_nodes = []
 | 
			
		||||
            deployed_nodes_count = 0
 | 
			
		||||
            exceptions.handle(self.request,
 | 
			
		||||
                              _('Unable to retrieve deployed nodes count.'))
 | 
			
		||||
        return deployed_nodes_count
 | 
			
		||||
 | 
			
		||||
    def get_deployed_nodes_data(self):
 | 
			
		||||
        try:
 | 
			
		||||
            deployed_nodes = api.Node.list(self.request, associated=True)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            deployed_nodes = []
 | 
			
		||||
            redirect = urlresolvers.reverse(
 | 
			
		||||
                'horizon:infrastructure:nodes:index')
 | 
			
		||||
            exceptions.handle(self.request,
 | 
			
		||||
                              _('Unable to retrieve resource nodes.'),
 | 
			
		||||
                              _('Unable to retrieve deployed nodes.'),
 | 
			
		||||
                              redirect=redirect)
 | 
			
		||||
        return resource_nodes
 | 
			
		||||
        return deployed_nodes
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FreeTab(tabs.TableTab):
 | 
			
		||||
@@ -74,6 +87,16 @@ class FreeTab(tabs.TableTab):
 | 
			
		||||
    slug = "free"
 | 
			
		||||
    template_name = ("horizon/common/_detail_table.html")
 | 
			
		||||
 | 
			
		||||
    def get_items_count(self):
 | 
			
		||||
        try:
 | 
			
		||||
            free_nodes_count = len(api.Node.list(self.request,
 | 
			
		||||
                                                 associated=False))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            free_nodes_count = "0"
 | 
			
		||||
            exceptions.handle(self.request,
 | 
			
		||||
                              _('Unable to retrieve free nodes count.'))
 | 
			
		||||
        return free_nodes_count
 | 
			
		||||
 | 
			
		||||
    def get_free_nodes_data(self):
 | 
			
		||||
        try:
 | 
			
		||||
            free_nodes = api.Node.list(self.request, associated=False)
 | 
			
		||||
@@ -89,5 +112,6 @@ class FreeTab(tabs.TableTab):
 | 
			
		||||
 | 
			
		||||
class NodeTabs(tabs.TabGroup):
 | 
			
		||||
    slug = "nodes"
 | 
			
		||||
    tabs = (OverviewTab, ResourceTab, FreeTab)
 | 
			
		||||
    tabs = (OverviewTab, DeployedTab, FreeTab)
 | 
			
		||||
    sticky = True
 | 
			
		||||
    template_name = "horizon/common/_items_count_tab_group.html"
 | 
			
		||||
 
 | 
			
		||||
@@ -2,23 +2,78 @@
 | 
			
		||||
{% load url from future%}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<div class="row-fluid"><div class="span12">
 | 
			
		||||
<p>
 | 
			
		||||
  <h4><span class="big-number">{{ nodes_total|default:0 }}</span> {% trans 'nodes in total' %}</h4>
 | 
			
		||||
</p>
 | 
			
		||||
<hr>
 | 
			
		||||
<p>
 | 
			
		||||
    <a href="{% url 'horizon:infrastructure:nodes:index' %}?tab=nodes__resource"
 | 
			
		||||
        ><span class="big-number">{{ nodes_resources|default:0 }}</span> {% trans 'Resource Nodes' %}</a>
 | 
			
		||||
</p>
 | 
			
		||||
<hr>
 | 
			
		||||
<p>
 | 
			
		||||
<a href="{% url 'horizon:infrastructure:nodes:register' %}"
 | 
			
		||||
   class="btn ajax-modal pull-right">{% trans 'Register Nodes' %}</a>
 | 
			
		||||
    <a href="{% url 'horizon:infrastructure:nodes:index' %}?tab=nodes__free"
 | 
			
		||||
        ><span class="big-number">{{ nodes_free|default:0 }}</span> {% trans 'Free Nodes' %}</a>
 | 
			
		||||
</p>
 | 
			
		||||
<hr>
 | 
			
		||||
<h4>{% trans 'Statistics' %}</h4>
 | 
			
		||||
<p>{% trans 'No statistics available' %}</p>
 | 
			
		||||
</div></div>
 | 
			
		||||
<div class="row-fluid">
 | 
			
		||||
  <div class="span8">
 | 
			
		||||
    <div class="widget">
 | 
			
		||||
      <h2>{% trans 'Health Status' %}</h2>
 | 
			
		||||
      <table class="table">
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td>
 | 
			
		||||
              <a href="{% url 'horizon:infrastructure:nodes:index' %}?tab=nodes__deployed">
 | 
			
		||||
                {% trans 'Deployed Nodes' %}
 | 
			
		||||
                ({{ deployed_nodes|length|default:0 }})
 | 
			
		||||
              </a>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>
 | 
			
		||||
              {% if deployed_nodes_down %}
 | 
			
		||||
                <i class="icon-exclamation-sign"></i>
 | 
			
		||||
                {% if deployed_nodes_down|length == 1 %}
 | 
			
		||||
                  {% url 'horizon:infrastructure:nodes:detail' deployed_nodes_down.0.uuid as node_detail_url %}
 | 
			
		||||
                {% else %}
 | 
			
		||||
                  {% url 'horizon:infrastructure:nodes:index' as nodes_index_url %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
 | 
			
		||||
                {% blocktrans count down_count=deployed_nodes_down|length %}
 | 
			
		||||
                  <a href="{{ node_detail_url }}">{{ down_count }} node</a> is down
 | 
			
		||||
                {% plural %}
 | 
			
		||||
                  <a href="{{ nodes_index_url }}?tab=nodes__deployed">{{ down_count }} nodes</a> are down
 | 
			
		||||
                {% endblocktrans %}
 | 
			
		||||
              {% else %}
 | 
			
		||||
                <i class="icon-ok"></i>
 | 
			
		||||
                {% trans 'All nodes are performing correctly' %}
 | 
			
		||||
              {% endif %}
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td>
 | 
			
		||||
              <a href="{% url 'horizon:infrastructure:nodes:index' %}?tab=nodes__free">
 | 
			
		||||
                {% trans 'Free Nodes' %}
 | 
			
		||||
                ({{ free_nodes|length|default:0 }})
 | 
			
		||||
              </a>
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>
 | 
			
		||||
              {% if free_nodes_down %}
 | 
			
		||||
                <i class="icon-exclamation-sign"></i>
 | 
			
		||||
                {% if free_nodes_down|length == 1 %}
 | 
			
		||||
                  {% url 'horizon:infrastructure:nodes:detail' free_nodes_down.0.uuid as node_detail_url %}
 | 
			
		||||
                {% else %}
 | 
			
		||||
                  {% url 'horizon:infrastructure:nodes:index' as nodes_index_url %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
 | 
			
		||||
                {% blocktrans count down_count=free_nodes_down|length %}
 | 
			
		||||
                  <a href="{{ node_detail_url }}">{{ down_count }} node</a> is down
 | 
			
		||||
                {% plural %}
 | 
			
		||||
                  <a href="{{ nodes_index_url }}?tab=nodes__free">{{ down_count }} nodes</a> are down
 | 
			
		||||
                {% endblocktrans %}
 | 
			
		||||
              {% else %}
 | 
			
		||||
                <i class="icon-ok"></i>
 | 
			
		||||
                {% trans 'All nodes are performing correctly' %}
 | 
			
		||||
              {% endif %}
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="span4">
 | 
			
		||||
    <div class="widget">
 | 
			
		||||
      <h2>{% trans 'Provisioning Status' %}</h2>
 | 
			
		||||
      <p>{% trans 'No statistics available' %}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="widget">
 | 
			
		||||
      <h2>{% trans 'Power Status' %}</h2>
 | 
			
		||||
      <p>{% trans 'No statistics available' %}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,20 @@
 | 
			
		||||
{% extends 'infrastructure/base.html' %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load url from future %}
 | 
			
		||||
{% block title %}{% trans 'Nodes' %}{% endblock %}
 | 
			
		||||
{% block title %}{% trans 'Registered Nodes' %}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block page_header %}
 | 
			
		||||
  {% include 'horizon/common/_domain_page_header.html' with title=_('Nodes') %}
 | 
			
		||||
  {% include 'horizon/common/_items_count_domain_page_header.html' with title=_('Registered Nodes') items_count=nodes_count %}
 | 
			
		||||
{% endblock page_header %}
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
<div class="row-fluid">
 | 
			
		||||
  <div class="span12">
 | 
			
		||||
    <div class="actions pull-right">
 | 
			
		||||
      <a href="{% url 'horizon:infrastructure:nodes:register' %}" class="btn ajax-modal">
 | 
			
		||||
        {% trans 'Register Nodes' %}
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
    {{ tab_group.render }}
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ class NodesTests(test.BaseAdminViewTests):
 | 
			
		||||
            'list.return_value': free_nodes,
 | 
			
		||||
        }) as mock:
 | 
			
		||||
            res = self.client.get(INDEX_URL + '?tab=nodes__free')
 | 
			
		||||
            self.assertEqual(mock.list.call_count, 4)
 | 
			
		||||
            self.assertEqual(mock.list.call_count, 10)
 | 
			
		||||
 | 
			
		||||
        self.assertTemplateUsed(res,
 | 
			
		||||
                                'infrastructure/nodes/index.html')
 | 
			
		||||
@@ -59,34 +59,34 @@ class NodesTests(test.BaseAdminViewTests):
 | 
			
		||||
            'list.side_effect': self.exceptions.tuskar,
 | 
			
		||||
        }) as mock:
 | 
			
		||||
            res = self.client.get(INDEX_URL + '?tab=nodes__free')
 | 
			
		||||
            self.assertEqual(mock.list.call_count, 3)
 | 
			
		||||
            self.assertEqual(mock.list.call_count, 2)
 | 
			
		||||
 | 
			
		||||
        self.assertRedirectsNoFollow(res, INDEX_URL)
 | 
			
		||||
 | 
			
		||||
    def test_resource_nodes(self):
 | 
			
		||||
        resource_nodes = [api.Node(node)
 | 
			
		||||
    def test_deployed_nodes(self):
 | 
			
		||||
        deployed_nodes = [api.Node(node)
 | 
			
		||||
                          for node in self.ironicclient_nodes.list()]
 | 
			
		||||
 | 
			
		||||
        with patch('tuskar_ui.api.Node', **{
 | 
			
		||||
            'spec_set': ['list'],  # Only allow these attributes
 | 
			
		||||
            'list.return_value': resource_nodes,
 | 
			
		||||
            'list.return_value': deployed_nodes,
 | 
			
		||||
        }) as mock:
 | 
			
		||||
            res = self.client.get(INDEX_URL + '?tab=nodes__resource')
 | 
			
		||||
            self.assertEqual(mock.list.call_count, 4)
 | 
			
		||||
            res = self.client.get(INDEX_URL + '?tab=nodes__deployed')
 | 
			
		||||
            self.assertEqual(mock.list.call_count, 10)
 | 
			
		||||
 | 
			
		||||
        self.assertTemplateUsed(
 | 
			
		||||
            res, 'infrastructure/nodes/index.html')
 | 
			
		||||
        self.assertTemplateUsed(res, 'horizon/common/_detail_table.html')
 | 
			
		||||
        self.assertItemsEqual(res.context['resource_nodes_table'].data,
 | 
			
		||||
                              resource_nodes)
 | 
			
		||||
        self.assertItemsEqual(res.context['deployed_nodes_table'].data,
 | 
			
		||||
                              deployed_nodes)
 | 
			
		||||
 | 
			
		||||
    def test_resource_nodes_list_exception(self):
 | 
			
		||||
    def test_deployed_nodes_list_exception(self):
 | 
			
		||||
        with patch('tuskar_ui.api.Node', **{
 | 
			
		||||
            'spec_set': ['list'],
 | 
			
		||||
            'list.side_effect': self.exceptions.tuskar,
 | 
			
		||||
        }) as mock:
 | 
			
		||||
            res = self.client.get(INDEX_URL + '?tab=nodes__resource')
 | 
			
		||||
            self.assertEqual(mock.list.call_count, 3)
 | 
			
		||||
            res = self.client.get(INDEX_URL + '?tab=nodes__deployed')
 | 
			
		||||
            self.assertEqual(mock.list.call_count, 2)
 | 
			
		||||
 | 
			
		||||
        self.assertRedirectsNoFollow(res, INDEX_URL)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,36 @@ class IndexView(horizon_tabs.TabbedTableView):
 | 
			
		||||
    tab_group_class = tabs.NodeTabs
 | 
			
		||||
    template_name = 'infrastructure/nodes/index.html'
 | 
			
		||||
 | 
			
		||||
    def get_free_nodes_count(self):
 | 
			
		||||
        try:
 | 
			
		||||
            free_nodes_count = len(api.Node.list(self.request,
 | 
			
		||||
                                                 associated=False))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            free_nodes_count = 0
 | 
			
		||||
            exceptions.handle(self.request,
 | 
			
		||||
                              _('Unable to retrieve free nodes.'))
 | 
			
		||||
        return free_nodes_count
 | 
			
		||||
 | 
			
		||||
    def get_deployed_nodes_count(self):
 | 
			
		||||
        try:
 | 
			
		||||
            deployed_nodes_count = len(api.Node.list(self.request,
 | 
			
		||||
                                                     associated=True))
 | 
			
		||||
        except Exception:
 | 
			
		||||
            deployed_nodes_count = 0
 | 
			
		||||
            exceptions.handle(self.request,
 | 
			
		||||
                              _('Unable to retrieve deployed nodes.'))
 | 
			
		||||
        return deployed_nodes_count
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super(IndexView, self).get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        context['free_nodes_count'] = self.get_free_nodes_count()
 | 
			
		||||
        context['deployed_nodes_count'] = self.get_deployed_nodes_count()
 | 
			
		||||
        context['nodes_count'] = (context['free_nodes_count'] +
 | 
			
		||||
                                  context['deployed_nodes_count'])
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RegisterView(horizon_forms.ModalFormView):
 | 
			
		||||
    form_class = forms.NodeFormset
 | 
			
		||||
 
 | 
			
		||||
@@ -522,10 +522,6 @@ input {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.big-number {
 | 
			
		||||
  font-size: 1.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#nodes-formset-datatable .datatable tbody {
 | 
			
		||||
  input {
 | 
			
		||||
    padding: 2px 1px;
 | 
			
		||||
@@ -587,6 +583,6 @@ input {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fullscreen-workflow-buttons {
 | 
			
		||||
.actions {
 | 
			
		||||
    margin: @baseLineHeight / 3;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% block page_header %}
 | 
			
		||||
  <div class='page-header'>
 | 
			
		||||
    <h2>
 | 
			
		||||
    {% if request.session.domain_context_name %}
 | 
			
		||||
      <em>{{ request.session.domain_context_name }}:</em>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% if items_count %}
 | 
			
		||||
      {{ items_count }}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {{ title }}
 | 
			
		||||
    </h2>
 | 
			
		||||
  </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
{% with tab_group.get_tabs as tabs %}
 | 
			
		||||
{% if tabs %}
 | 
			
		||||
 | 
			
		||||
  {# Tab Navigation #}
 | 
			
		||||
  <ul id="{{ tab_group.get_id }}" {{ tab_group.attr_string|safe }}>
 | 
			
		||||
    {% for tab in tabs %}
 | 
			
		||||
    <li {{ tab.attr_string|safe }}>
 | 
			
		||||
      <a href="?{{ tab_group.param_name}}={{ tab.get_id }}" data-toggle="tab" data-target="#{{ tab.get_id }}" data-loaded='{{ tab.load|yesno:"true,false" }}'>
 | 
			
		||||
        {{ tab.name }} {% if tab.get_items_count %} ({{ tab.get_items_count }}) {% endif %}
 | 
			
		||||
      </a>
 | 
			
		||||
    </li>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
  </ul>
 | 
			
		||||
 | 
			
		||||
  {# Tab Content #}
 | 
			
		||||
  <div class="tab-content">
 | 
			
		||||
    {% for tab in tabs %}
 | 
			
		||||
    <div id="{{ tab.get_id }}" class="tab-pane{% if tab.is_active %} active{% endif %}">
 | 
			
		||||
      {{ tab.render }}
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
  <form class="form form-horizontal" {{ workflow.attr_string|safe }} action="{{ workflow.get_absolute_url }}" {% if add_to_field %}data-add-to-field="{{ add_to_field }}"{% endif %} method="POST"{% if workflow.multipart %} enctype="multipart/form-data"{% endif %}>
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% if REDIRECT_URL %}<input type="hidden" name="{{ workflow.redirect_param_name }}" value="{{ REDIRECT_URL }}"/>{% endif %}
 | 
			
		||||
    <div class="fullscreen-workflow-buttons pull-right">
 | 
			
		||||
    <div class="actions pull-right">
 | 
			
		||||
    {% block workflow-buttons %}
 | 
			
		||||
        <input class="btn btn-primary pull-right" type="submit" value="{{ workflow.finalize_button_name }}">
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user