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:
Jiri Tomasek
2014-01-27 08:17:49 -05:00
parent 17f8899de2
commit 2b664755a2
10 changed files with 214 additions and 65 deletions

View File

@@ -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 = ()

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}