Merge lp:~jtv/maas/commissioning-results-ui-tweaks into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 1963
Proposed branch: lp:~jtv/maas/commissioning-results-ui-tweaks
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 358 lines (+105/-66)
6 files modified
src/maasserver/templates/metadataserver/nodecommissionresult.html (+8/-6)
src/maasserver/templates/metadataserver/nodecommissionresult_list.html (+33/-22)
src/maasserver/views/nodecommissionresult.py (+15/-1)
src/maasserver/views/nodes.py (+1/-9)
src/maasserver/views/tests/test_nodecommissionresults.py (+48/-12)
src/maasserver/views/tests/test_nodes.py (+0/-16)
To merge this branch: bzr merge lp:~jtv/maas/commissioning-results-ui-tweaks
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+206732@code.launchpad.net

Commit message

Tweaks for the commissioning-results UI, as requested in previous review: indicate whether nodes listing is being filtered by node (and which), make leftmost column in listing (rather than rightmost) link to the details page, and make the "time" and "script result" fields on the details page less verbose.

Description of the change

These cover the remaining review notes from the original review for this UI: https://code.launchpad.net/~jtv/maas/show-commissioning-results/+merge/206661

We have functions similar to normalise_whitespace elsewhere. I figured it wasn't _quite_ worth centralising, given that "normalise" is a bit vague and only takes on meaning in the context of the module, but it's getting awfully close to that point where we want something reusable.

Jeroen

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :
Download full text (3.4 KiB)

Looks good. I have lots of comments, but they're mostly around the UI,
and not your changes. Pick out those ones that you want to fix; the rest
is just my commentary on the feature.

[1]

From IRC:

<jtv> allenap: if you want to play with the new feature, edit
  src/maasserver/views/nodes.py and change ENABLE_NODECOMMISSIONRESULTS
  to be True.

Why are we using feature flags in trunk?

[2]

+              {% if result_item.script_result == 0 %}
+              <span>OK: {{ result_item.script_result }}</span>
+              {% else %}

I know this probably comes under "systemtic UI review", but I don't
think this needs to include the return code. Just "OK" is enough.

+              <span class="warning">
+                <img src="{{ STATIC_URL }}img/warning.png" title="Failed" />
+                FAILED: {{ result_item.script_result }}
+              </span>
+              {% endif %}

Likewise, this doesn't need the return code (it ought to be on the
details page though).

[3]

On the details page, instead of having the 2-column thing at the top,
which takes up a lot of space without telling us much, how about
integrating it into the output:

  <breadcrumbs>
    Nodes > moon > Commissioning results > 00-maas-01-lshw.out
  </breadcrumbs>

  <heading>
    FAILED
  </heading>

  <output>
    <italics>
      Recorded 12:34 on February 17th, 2014.
    </italics>
    Error while installing lldp: Network unreachable.
    <italics>
      Exited 1
    </italics>
  </output>

Maybe that's no good either. I just know that I dislike that 2-column
layout; it's too sparse and light on useful information.

[4]

On http://localhost:5240/commissioning-results/?node=... I expected to
be able to click on the "/!\ FAILED" bit to see the logs.  If I had
wanted to see the logs from an "OK" result I would have wanted to click
in the same column; it is called "Script result" after all.

[4.1]

Same goes for http://localhost:5240/nodes/; I want to be able to click
the "Failed tests" status line or the warning triangle to see what's
wrong.

[4.2]

The URL http://localhost:5240/commissioning-results/?node=... is
slightly disorientating. It would be good to have a view under
/nodes/$id/ to show a page with that node's own results.

[5]

I'm also not sure I care what file it wrote to:
00-maas-02-virtuality.out means little to me, and I know the
implementation.

[6]

I'm also not sure the Node column is useful on that page. I see that
it's a general commissioning results page, narrowed to that particular
node, but I got there by finding "moon" then clicking "3 output files".
To be told that it's for node-29d7ad70-4671-11e1-93b8-00225f89f211 is
confusing. Once I understood, it was superfluous.

I guess it needs to be there when showing more than just a single node's
results, but it should then show the node's given name, rather than its
system-generated name.

[7]

On http://localhost:5240/nodes/.../view/ the "Commissioning output" cell
should probably show if commissioning failed too, but perhaps that's
coming later.

[8]

It's not a change you made, but the <pre> block for the output has
excess whitespace within it. <pre> is so anal that it's necessary to
foreg...

Read more...

review: Approve
Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Monday 17 Feb 2014 15:59:06 you wrote:
> <jtv> allenap: if you want to play with the new feature, edit
> src/maasserver/views/nodes.py and change ENABLE_NODECOMMISSIONRESULTS
> to be True.
>
> Why are we using feature flags in trunk?

Let's get rid of the flag. The feature will be useful to people, even if it's
not polished to the shine of Gavin's head. We'll also get early feedback from
users before release time.

cheers.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (5.8 KiB)

> Looks good. I have lots of comments, but they're mostly around the UI,
> and not your changes. Pick out those ones that you want to fix; the rest
> is just my commentary on the feature.

Thanks for going through it so thoroughly! I made lots of changes in response, and they brought me to the point where I'm confident enough to drop the feature flag.

> [1]
>
> From IRC:
>
> <jtv> allenap: if you want to play with the new feature, edit
>  src/maasserver/views/nodes.py and change ENABLE_NODECOMMISSIONRESULTS
>  to be True.
>
> Why are we using feature flags in trunk?

Because the change would have been much more contentious otherwise. It was a mad-dash personal initiative, not a carefully planned feature. Planned as a feature it would have taken longer, and therefore, been lower on the priority list and impossible to fit in before the sprint. As a mad dash, it was at risk of bogging down in design/review changes, and we might still have ended up getting something great later instead of something good now. The feature flag made it possible to land the initial branch and make changes later.

> [2]
>
> +              {% if result_item.script_result == 0 %}
> +              <span>OK: {{ result_item.script_result }}</span>
> +              {% else %}
>
> I know this probably comes under "systemtic UI review", but I don't
> think this needs to include the return code. Just "OK" is enough.

Exactly the sort of feedback I needed. Done.

> +              <span class="warning">
> +                <img src="{{ STATIC_URL }}img/warning.png" title="Failed" />
> +                FAILED: {{ result_item.script_result }}
> +              </span>
> +              {% endif %}
>
> Likewise, this doesn't need the return code (it ought to be on the
> details page though).

Also done.

> [3]
>
> On the details page, instead of having the 2-column thing at the top,
> which takes up a lot of space without telling us much, how about
> integrating it into the output:
>
>  <breadcrumbs>
>    Nodes > moon > Commissioning results > 00-maas-01-lshw.out
>  </breadcrumbs>
>
>  <heading>
>    FAILED
>  </heading>
>
>  <output>
>    <italics>
>      Recorded 12:34 on February 17th, 2014.
>    </italics>
>    Error while installing lldp: Network unreachable.
>    <italics>
>      Exited 1
>    </italics>
>  </output>
>
> Maybe that's no good either. I just know that I dislike that 2-column
> layout; it's too sparse and light on useful information.

I agree, but it's what we had available... Maybe we can have a proper redesign for this sometime after the sprint. It'd be nice if it didn't look quite so much like a Node page. For now though I hope this will remain a fairly obscure page.

> [4]
>
> On http://localhost:5240/commissioning-results/?node=... I expected to
> be able to click on the "/!\ FAILED" bit to see the logs.  If I had
> wanted to see the logs from an "OK" result I would have wanted to click
> in the same column; it is called "Script result" after all.

Ironically, that's what I had in my original branch. But Raphaël preferred the link on the left-hand column, so I changed it. And now I have a solution that I hope will ...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/templates/metadataserver/nodecommissionresult.html'
2--- src/maasserver/templates/metadataserver/nodecommissionresult.html 2014-02-17 07:34:05 +0000
3+++ src/maasserver/templates/metadataserver/nodecommissionresult.html 2014-02-18 04:37:34 +0000
4@@ -19,19 +19,21 @@
5 </span>
6 </li>
7 <li class="block size7 first" id="time">
8- <h4>Time</h4>
9- <span>Result was registered at {{ object.created }}</span>
10+ <h4>Result registered at</h4>
11+ <span>{{ object.created }}</span>
12 </li>
13 <li class="block size3" id="script-result">
14- <h4>Return value</h4>
15- <span>Script returned {{ object.script_result }}</span>
16+ <h4>Script return value</h4>
17+ <span>{{ object.script_result }}</span>
18 </li>
19
20 <li class="block first separate" id="output">
21+ {% if object.data %}
22 <h2>Output</h2>
23- <pre>
24+<pre>
25 {{ object.get_data_as_html }}
26- </pre>
27+</pre>
28+ {% endif %}
29 </li>
30 </ul>
31
32
33=== modified file 'src/maasserver/templates/metadataserver/nodecommissionresult_list.html'
34--- src/maasserver/templates/metadataserver/nodecommissionresult_list.html 2014-02-17 04:25:33 +0000
35+++ src/maasserver/templates/metadataserver/nodecommissionresult_list.html 2014-02-18 04:37:34 +0000
36@@ -1,6 +1,6 @@
37 {% extends "maasserver/base.html" %}
38
39-{% block nav-active-zone-list %}active{% endblock %}
40+{% block nav-active-result-list %}active{% endblock %}
41 {% block title %}Node commissioning results{% endblock %}
42 {% block page-title %}
43 {{ paginator.count }} result{{ paginator.count|pluralize }}
44@@ -12,41 +12,52 @@
45
46 {% block content %}
47 <div id="results">
48- <h2>Commissioning results</h2>
49+ <h2 id="header">
50+ Commissioning results
51+ {% if nodes_filter %}
52+ for {{ nodes_filter }}
53+ {% endif %}
54+ </h2>
55 {% if paginator.count == 0 %}
56 <p>No matching commissioning results.</p>
57 {% else %}
58 <table class="list">
59 <thead>
60 <tr>
61- <th>Time</th>
62+ <th>Script result</th>
63+ <th>Output file</th>
64+ <th>Registered at</th>
65 <th>Node</th>
66- <th>Output file</th>
67- <th>Script result<th>
68 </tr>
69 </thead>
70 <tbody>
71 {% for result_item in results_list %}
72 <tr class="result {% cycle 'even' 'odd' %}">
73- <td>{{ result_item.created }}</td>
74+ <!-- Script result -->
75+ <td>
76+ <a href="{% url 'nodecommissionresult-view' result_item.id %}">
77+ {% if result_item.script_result == 0 %}
78+ <span>OK</span>
79+ {% else %}
80+ <span class="warning">
81+ <img src="{{ STATIC_URL }}img/warning.png" title="Failed" />
82+ FAILED
83+ </span>
84+ {% endif %}
85+ </a>
86+ </td>
87+ <!-- Output file -->
88+ <td>
89+ {{ result_item.name }}
90+ </td>
91+ <!-- Registered at -->
92+ <td>
93+ {{ result_item.created }}
94+ </td>
95+ <!-- Node -->
96 <td>
97 <a href="{% url 'node-view' result_item.node.system_id %}">
98- {{ result_item.node.system_id }}
99- </a>
100- </td>
101- <td>
102- {{ result_item.name }}
103- </td>
104- <td>
105- <a href="{% url 'nodecommissionresult-view' result_item.id %}">
106- {% if result_item.script_result == 0 %}
107- OK: {{ result_item.script_result }}
108- {% else %}
109- <span class="warning">
110- <img src="{{ STATIC_URL }}img/warning.png" title="Failed" />
111- FAILED: {{ result_item.script_result }}
112- </span>
113- {% endif %}
114+ {{ result_item.node.hostname }}
115 </a>
116 </td>
117 </tr>
118
119=== modified file 'src/maasserver/views/nodecommissionresult.py'
120--- src/maasserver/views/nodecommissionresult.py 2014-02-17 12:28:55 +0000
121+++ src/maasserver/views/nodecommissionresult.py 2014-02-18 04:37:34 +0000
122@@ -18,6 +18,7 @@
123
124 from django.shortcuts import get_object_or_404
125 from django.views.generic import DetailView
126+from maasserver.models import Node
127 from maasserver.views import PaginatedListView
128 from metadataserver.models import NodeCommissionResult
129
130@@ -27,9 +28,22 @@
131 template_name = 'maasserver/nodecommissionresult-list.html'
132 context_object_name = 'results_list'
133
134+ def get_filter_system_ids(self):
135+ """Return the list of nodes that were selected for filtering."""
136+ return self.request.GET.getlist('node')
137+
138+ def get_context_data(self, **kwargs):
139+ context = super(NodeCommissionResultListView, self).get_context_data()
140+ system_ids = self.get_filter_system_ids()
141+ if system_ids is not None and len(system_ids) > 0:
142+ nodes = Node.objects.filter(system_id__in=system_ids)
143+ context['nodes_filter'] = ', '.join(
144+ sorted(node.hostname for node in nodes))
145+ return context
146+
147 def get_queryset(self):
148 results = NodeCommissionResult.objects.all()
149- system_ids = self.request.GET.getlist('node')
150+ system_ids = self.get_filter_system_ids()
151 if system_ids is not None and len(system_ids) > 0:
152 results = results.filter(node__system_id__in=system_ids)
153 return results.order_by('node', '-created', 'name')
154
155=== modified file 'src/maasserver/views/nodes.py'
156--- src/maasserver/views/nodes.py 2014-02-13 10:29:27 +0000
157+++ src/maasserver/views/nodes.py 2014-02-18 04:37:34 +0000
158@@ -400,11 +400,6 @@
159 """)
160
161
162-# Feature flag: link to node commissioning results?
163-# XXX jtv 2014-02-13: Remove this when this UI feature is good enough.
164-ENABLE_NODECOMMISSIONRESULTS = False
165-
166-
167 class NodeView(NodeViewMixin, UpdateView):
168 """View class to display a node's information and buttons for the actions
169 which can be performed on this node.
170@@ -447,10 +442,7 @@
171 context["probed_details"] = etree.tostring(
172 probed_details, encoding=unicode, pretty_print=True)
173
174- if ENABLE_NODECOMMISSIONRESULTS:
175- results = NodeCommissionResult.objects.filter(node=node).count()
176- else:
177- results = 0
178+ results = NodeCommissionResult.objects.filter(node=node).count()
179 context['nodecommissionresults'] = results
180
181 return context
182
183=== modified file 'src/maasserver/views/tests/test_nodecommissionresults.py'
184--- src/maasserver/views/tests/test_nodecommissionresults.py 2014-02-17 12:28:55 +0000
185+++ src/maasserver/views/tests/test_nodecommissionresults.py 2014-02-18 04:37:34 +0000
186@@ -30,6 +30,11 @@
187 from testtools.matchers import HasLength
188
189
190+def normalise_whitespace(text):
191+ """Replace any whitespace sequence with just a single space."""
192+ return ' '.join(text.split())
193+
194+
195 class TestNodeCommissionResultView(MAASServerTestCase):
196
197 def request_page(self, result):
198@@ -72,7 +77,7 @@
199 result.node.hostname,
200 self.extract_field(doc, 'node'))
201 self.assertEqual(
202- "Script returned %d" % result.script_result,
203+ "%d" % result.script_result,
204 self.extract_field(doc, 'script-result'))
205 self.assertEqual(result.data, self.extract_field(doc, 'output', 'pre'))
206
207@@ -92,6 +97,13 @@
208 doc = self.request_page(result)
209 self.assertEqual('A\ufffdB', self.extract_field(doc, 'output', 'pre'))
210
211+ def test_hides_output_if_empty(self):
212+ self.client_log_in(as_admin=True)
213+ result = factory.make_node_commission_result(data=b'')
214+ doc = self.request_page(result)
215+ field = get_one(doc.cssselect('#output'))
216+ self.assertEqual('', field.text_content().strip())
217+
218
219 class TestNodeCommissionResultListView(MAASServerTestCase):
220
221@@ -143,26 +155,23 @@
222 result_row = get_one(content.cssselect('.result'))
223 fields = result_row.cssselect('td')
224
225- [time, node, output_file, script_result] = fields
226+ [script_result, output_file, time, node] = fields
227+ self.assertEqual('OK', script_result.text_content().strip())
228 self.assertIn('%d' % result.created.year, time.text_content())
229- self.assertEqual(result.node.system_id, node.text_content().strip())
230+ self.assertEqual(result.node.hostname, node.text_content().strip())
231 self.assertEqual(
232 reverse('node-view', args=[result.node.system_id]),
233 get_one(node.cssselect('a')).get('href'))
234 self.assertEqual(result.name, output_file.text_content().strip())
235- self.assertEqual('OK: 0', script_result.text_content().strip())
236
237 def test_shows_failure(self):
238 self.client_log_in(as_admin=True)
239- result = factory.make_node_commission_result(
240- script_result=randint(1, 100))
241+ factory.make_node_commission_result(script_result=randint(1, 100))
242 content = self.request_page()
243 result_row = get_one(content.cssselect('.result'))
244 fields = result_row.cssselect('td')
245- [_, _, _, script_result] = fields
246- self.assertEqual(
247- "FAILED: %d" % result.script_result,
248- script_result.text_content().strip())
249+ [script_result, _, _, _] = fields
250+ self.assertEqual("FAILED", script_result.text_content().strip())
251 self.assertNotEqual([], script_result.find_class('warning'))
252
253 def test_links_to_result(self):
254@@ -172,7 +181,7 @@
255 content = self.request_page()
256 result_row = get_one(content.cssselect('.result'))
257 fields = result_row.cssselect('td')
258- [_, _, _, script_result] = fields
259+ [script_result, _, _, _] = fields
260 link = get_one(script_result.cssselect('a'))
261 self.assertEqual(
262 reverse('nodecommissionresult-view', args=[result.id]),
263@@ -241,8 +250,35 @@
264 self.assertThat(rows, HasLength(len(matching_results)))
265 matching_names = set()
266 for row in rows:
267- [_, _, name, _] = row.cssselect('td')
268+ [_, name, _, _] = row.cssselect('td')
269 matching_names.add(name.text_content().strip())
270 self.assertEqual(
271 {result.name for result in matching_results},
272 matching_names)
273+
274+ def test_does_not_show_node_if_not_filtering_by_node(self):
275+ self.client_log_in(as_admin=True)
276+ doc = self.request_page()
277+ header = get_one(doc.cssselect('#header'))
278+ self.assertEqual(
279+ "Commissioning results",
280+ normalise_whitespace(header.text_content()))
281+
282+ def test_shows_node_if_filtering_by_node(self):
283+ self.client_log_in(as_admin=True)
284+ node = factory.make_node()
285+ doc = self.request_page(nodes=[node])
286+ header = get_one(doc.cssselect('#header'))
287+ self.assertEqual(
288+ "Commissioning results for %s" % node.hostname,
289+ normalise_whitespace(header.text_content()))
290+
291+ def test_shows_nodes_if_filtering_by_multiple_nodes(self):
292+ self.client_log_in(as_admin=True)
293+ names = [factory.make_name('node').lower() for _ in range(2)]
294+ nodes = [factory.make_node(hostname=name) for name in names]
295+ doc = self.request_page(nodes=nodes)
296+ header = get_one(doc.cssselect('#header'))
297+ self.assertEqual(
298+ "Commissioning results for %s" % ', '.join(sorted(names)),
299+ normalise_whitespace(header.text_content()))
300
301=== modified file 'src/maasserver/views/tests/test_nodes.py'
302--- src/maasserver/views/tests/test_nodes.py 2014-02-13 10:53:27 +0000
303+++ src/maasserver/views/tests/test_nodes.py 2014-02-18 04:37:34 +0000
304@@ -951,11 +951,6 @@
305 class NodeCommissionResultsDisplayTest(MAASServerTestCase):
306 """Tests for the link to node commissioning results on the Node page."""
307
308- def enable_display(self):
309- """Enable the feature flag behind which this display is hidden."""
310- # XXX jtv 2014-02-13: Remove this once feature is good enough.
311- self.patch(nodes_views, 'ENABLE_NODECOMMISSIONRESULTS', True)
312-
313 def request_results_display(self, node):
314 """Request the page for `node`, and extract the results display.
315
316@@ -1000,7 +995,6 @@
317 return ' '.join(text.split())
318
319 def test_view_node_links_to_commissioning_results_if_appropriate(self):
320- self.enable_display()
321 self.client_log_in(as_admin=True)
322 result = factory.make_node_commission_result()
323 section = self.request_results_display(result.node)
324@@ -1010,26 +1004,17 @@
325 results_list + '?node=%s' % result.node.system_id,
326 link.get('href'))
327
328- def test_view_node_hides_commissioning_results_by_default(self):
329- # XXX jtv 2014-02-13: Remove this test once we get rid of feature flag.
330- self.client_log_in(as_admin=True)
331- result = factory.make_node_commission_result()
332- self.assertIsNone(self.request_results_display(result.node))
333-
334 def test_view_node_shows_commissioning_results_only_if_present(self):
335- self.enable_display()
336 self.client_log_in(as_admin=True)
337 node = factory.make_node()
338 self.assertIsNone(self.request_results_display(node))
339
340 def test_view_node_shows_commissioning_results_only_to_superuser(self):
341- self.enable_display()
342 self.client_log_in(as_admin=False)
343 result = factory.make_node_commission_result()
344 self.assertIsNone(self.request_results_display(result.node))
345
346 def test_view_node_shows_single_commissioning_result(self):
347- self.enable_display()
348 self.client_log_in(as_admin=True)
349 result = factory.make_node_commission_result()
350 section = self.request_results_display(result.node)
351@@ -1039,7 +1024,6 @@
352 self.normalise_whitespace(link.text_content()))
353
354 def test_view_node_shows_multiple_commissioning_results(self):
355- self.enable_display()
356 self.client_log_in(as_admin=True)
357 node = factory.make_node()
358 num_results = randint(2, 5)