Merge ~caleb-ellis/maas:suppress-single-test into maas:master
- Git
- lp:~caleb-ellis/maas
- suppress-single-test
- Merge into master
Status: | Merged |
---|---|
Approved by: | Caleb Ellis |
Approved revision: | 2b0be59e32348869d50db7646fdffa59091dc738 |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~caleb-ellis/maas:suppress-single-test |
Merge into: | maas:master |
Diff against target: |
589 lines (+392/-7) 12 files modified
src/maasserver/static/js/angular/controllers/node_results.js (+41/-1) src/maasserver/static/js/angular/controllers/tests/test_node_results.js (+148/-0) src/maasserver/static/js/angular/directives/machines_table.js (+1/-1) src/maasserver/static/js/angular/enum.js (+13/-2) src/maasserver/static/js/angular/factories/nodes.js (+20/-0) src/maasserver/static/js/angular/factories/tests/test_nodes.js (+42/-0) src/maasserver/static/partials/script-results-list.html (+34/-2) src/maasserver/static/scss/_base_tables.scss (+10/-0) src/maasserver/websockets/handlers/controller.py (+1/-0) src/maasserver/websockets/handlers/machine.py (+1/-0) src/maasserver/websockets/handlers/node.py (+14/-0) src/maasserver/websockets/handlers/tests/test_machine.py (+67/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Lee Trager (community) | Approve | ||
Anthony Dillon | Approve | ||
Blake Rouse (community) | Approve | ||
Review via email: mp+366513@code.launchpad.net |
Commit message
Allow dismissing tests from tests tab in machine details page
Description of the change
## Wireframe
https:/
## Done
- Added unsuppress_test method to node websocket handler
- Added a "Hide" column and checkbox to tests tab, which toggles suppressed parameter of a single **failed** test
- Added warning notification to tests tab if suppressed tests exist
## QA
- Log in as an admin
- Go to the hardware tests tab of a machine that has failed tests
- Hide a test and check that the row is slightly greyed out and a notification pops up
- Hide every failed test and check that the machine summary page no longer has red borders around the failed test suites. Note that the hardware tests tab icon will change to a green tick - in future this should be a warning icon but there isn't currently a testing status code for "some tests failed, but they were all suppressed"
- Log out and back in as a normal user
- Check that the hide test checkboxes are disabled and a tooltip shows up on hover
## Screenshots
### Default hardware tests page
https:/
### One test suppressed
https:/
### All tests suppressed
https:/
### Updated summary page
https:/
### Updated listing page
https:/
EDIT: Updated to have an "All" label and made the Hide col a bit thinner:
https:/
EDIT2: Updated so only admins can suppress/unsuppress tests. If user is a non-admin, the checkbox is disabled with a tooltip:
https:/
EDIT3: Added ScriptStatus to enum.js, and updated so only tests that have FAILED, FAILED_INSTALLING or TIMEDOUT can be suppressed.
https:/
EDIT4: Changed instances of "hide" to "suppress":
https:/
Martin Storey (cassiocassio) wrote : | # |
nice.
- The column width for the HIDE column is kind of wide for a single checkbox
- Should/could we special-case "All tests hidden" in the notification?
(scope creep)
- when you get to "Hide all tests" in the overide failed tests dialog, can you expose the same hide-all functionality with a checkbox at the top the HIDE column?
Lee Trager (ltrager) wrote : | # |
I agree with Blake that the column should be called Suppressed like it is on the API.
In master we have set_script_
Martin Storey (cassiocassio) wrote : | # |
I aggree for historical features that have established use in CLI and code,
that the GUI should reflect the API, so users who already know one can
recognise the feature as the same.
In this case, it's a new feature, and we can call it what we want, to
reflect the functionality.
--
Separate question. Is "supress" a good name? I can live with it, but I've
never liked it. It sounds more technical than it really is.
"Hide" is simpler, more honest language that's immediately understandable,
unambiguous.
Some of our language, eg. "commission" and "deploy" follow industry
practice, so they're the right word, even though they're slightly
ridiculous. You commission officers and deploy troops. I wonder why all
this language is so military?
So perhaps that's why "Suppress" - it's what the First Order does to The
Resistance?
But please, what we really mean here is "Hide".
On Fri, Apr 26, 2019 at 7:42 AM Lee Trager <email address hidden> wrote:
> Review: Needs Fixing
>
> I agree with Blake that the column should be called Suppressed like it is
> on the API.
>
> In master we have set_script_
> set_script_
> fix both in a separate bug. However the UI should be updated to also send
> the system_id.
> --
> https:/
> Your team MAAS UI team is requested to review the proposed merge of
> ~caleb-
>
Caleb Ellis (caleb-ellis) wrote : | # |
> I agree with Blake that the column should be called Suppressed like it is on
> the API.
>
> In master we have set_script_
> set_script_
> both in a separate bug. However the UI should be updated to also send the
> system_id.
Alright I've updated so the UI sends through the node system_id, and I'm okay to leave the permissions issue with you?
Blake Rouse (blake-rouse) : | # |
Caleb Ellis (caleb-ellis) wrote : | # |
Updated so only admins can suppress or unsuppress tests, otherwise a permission exception is raised. In the UI, the checkboxes become disabled for non-admins.
Lilyana Videnova (lilyanavidenova) wrote : | # |
Can we add a tooltip on those disabled checkboxes saying "Only admins can hide failed tests."
Caleb Ellis (caleb-ellis) wrote : | # |
Blake Rouse (blake-rouse) wrote : | # |
Thanks for the fixes, looks good.
Lee Trager (ltrager) wrote : | # |
I just tested this branch and if I click hide nothing happens, same if I try the option in the menu list. I also noticed that the 'hide' check box is shown for aborted and pending, installing, and running statuses. It should only be shown if the test has failed, failed installing, or timed out.
I still think we need to be consistent between in naming between the API and UI. Users often do something in the UI and then try to automate that process. The API currently calls this suppress and does not mention 'hiding' at all. This will leave users confused as to how to 'hide' a ScriptResult with the API. I'd be open to changing the API to 'hide' if we agree that is a better name however we have already released a few beta's with this being called suppress so changing at this point would cause API breakage.
Anthony Dillon (ya-bo-ng) wrote : | # |
Code looks good to me +1, so inline styles but nothing alarming.
Caleb Ellis (caleb-ellis) wrote : | # |
Lee, did you run `make force-javascript` beforehand? I've updated so only tests with status FAILED, FAILED_INSTALLING or TIMEDOUT can be suppressed via the UI. As for the table header being "Hide" I'll leave it as is for now - this was agreed upon in the design meetings and is in the 9th iteration of the wireframe (https:/
https:/
Blake Rouse (blake-rouse) : | # |
Caleb Ellis (caleb-ellis) : | # |
Blake Rouse (blake-rouse) : | # |
Lee Trager (ltrager) wrote : | # |
My bad I did make javascript instead of make force-javascript. Thanks for the fixes, LGTM!
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b suppress-
STATUS: FAILED BUILD
LOG: http://
- 2b0be59... by Caleb Ellis
-
Fix linting
Preview Diff
1 | diff --git a/src/maasserver/static/js/angular/controllers/node_results.js b/src/maasserver/static/js/angular/controllers/node_results.js |
2 | index 66a7c0d..6ebccb9 100644 |
3 | --- a/src/maasserver/static/js/angular/controllers/node_results.js |
4 | +++ b/src/maasserver/static/js/angular/controllers/node_results.js |
5 | @@ -1,4 +1,4 @@ |
6 | -import NodeResultController from "./node_result"; |
7 | +import { ScriptStatus } from "../enum"; |
8 | |
9 | /* Copyright 2017-2018 Canonical Ltd. This software is licensed under the |
10 | * GNU Affero General Public License version 3 (see the file LICENSE). |
11 | @@ -222,6 +222,46 @@ function NodeResultsController( |
12 | }); |
13 | }; |
14 | |
15 | + $scope.hasSuppressedTests = () => { |
16 | + return $scope.results.some(type => { |
17 | + const entries = Object.entries(type.results); |
18 | + return entries.some(entry => entry[1].some(result => result.suppressed)); |
19 | + }); |
20 | + }; |
21 | + |
22 | + $scope.isSuppressible = result => |
23 | + result.status === ScriptStatus.FAILED || |
24 | + result.status === ScriptStatus.FAILED_INSTALLING || |
25 | + result.status === ScriptStatus.TIMEDOUT; |
26 | + |
27 | + $scope.getSuppressedCount = () => { |
28 | + const suppressibleTests = $scope.results.reduce((acc, type) => { |
29 | + const entries = Object.entries(type.results); |
30 | + entries.forEach(entry => { |
31 | + entry[1].forEach(result => { |
32 | + if ($scope.isSuppressible(result)) { |
33 | + acc.push(result); |
34 | + } |
35 | + }); |
36 | + }); |
37 | + return acc; |
38 | + }, []); |
39 | + const suppressedTests = suppressibleTests.filter(test => test.suppressed); |
40 | + |
41 | + if (suppressibleTests.length === suppressedTests.length) { |
42 | + return "All"; |
43 | + } |
44 | + return suppressedTests.length; |
45 | + }; |
46 | + |
47 | + $scope.toggleSuppressed = result => { |
48 | + if (result.suppressed) { |
49 | + $scope.nodesManager.unsuppressTests($scope.node, [result]); |
50 | + } else { |
51 | + $scope.nodesManager.suppressTests($scope.node, [result]); |
52 | + } |
53 | + }; |
54 | + |
55 | // Destroy the NodeResultsManager when the scope is destroyed. This is |
56 | // so the client will not recieve any more notifications about results |
57 | // from this node. |
58 | diff --git a/src/maasserver/static/js/angular/controllers/tests/test_node_results.js b/src/maasserver/static/js/angular/controllers/tests/test_node_results.js |
59 | index 1839b07..c0712f2 100644 |
60 | --- a/src/maasserver/static/js/angular/controllers/tests/test_node_results.js |
61 | +++ b/src/maasserver/static/js/angular/controllers/tests/test_node_results.js |
62 | @@ -4,6 +4,21 @@ |
63 | * Unit tests for NodeResultsController |
64 | */ |
65 | |
66 | +// 2019-04-30 Caleb - Syntax error `import { ScriptStatus }from "../../enum"`; |
67 | +// TODO - Fix es module imports in test files |
68 | +const ScriptStatus = { |
69 | + PENDING: 0, |
70 | + RUNNING: 1, |
71 | + PASSED: 2, |
72 | + FAILED: 3, |
73 | + TIMEDOUT: 4, |
74 | + ABORTED: 5, |
75 | + DEGRADED: 6, |
76 | + INSTALLING: 7, |
77 | + FAILED_INSTALLING: 8, |
78 | + SKIPPED: 9 |
79 | +}; |
80 | + |
81 | describe("NodeResultsController", function() { |
82 | // Load the MAAS module. |
83 | beforeEach(module("MAAS")); |
84 | @@ -511,4 +526,137 @@ describe("NodeResultsController", function() { |
85 | expect($scope.nodeResultsManager.get_history).not.toHaveBeenCalled(); |
86 | }); |
87 | }); |
88 | + |
89 | + describe("hasSuppressedTests", () => { |
90 | + it("returns whether there are suppressed tests in results", () => { |
91 | + makeController(); |
92 | + const suppressedResult = makeResult(1); |
93 | + $scope.results = [ |
94 | + { |
95 | + hardware_type: 1, |
96 | + results: { |
97 | + subtype: [ |
98 | + ...Array.from(Array(3)).map(() => makeResult(1)), |
99 | + suppressedResult |
100 | + ] |
101 | + } |
102 | + }, |
103 | + { |
104 | + hardware_type: 2, |
105 | + results: { |
106 | + subtype: Array.from(Array(3)).map(() => makeResult(2)) |
107 | + } |
108 | + } |
109 | + ]; |
110 | + |
111 | + expect($scope.hasSuppressedTests()).toEqual(false); |
112 | + |
113 | + suppressedResult.suppressed = true; |
114 | + expect($scope.hasSuppressedTests()).toEqual(true); |
115 | + }); |
116 | + }); |
117 | + |
118 | + describe("isSuppressible", () => { |
119 | + it(`returns true if a result's status is |
120 | + FAILED, FAILED_INSTALLING or TIMEDOUT`, () => { |
121 | + makeController(); |
122 | + const results = [ |
123 | + makeResult(0, ScriptStatus.FAILED), |
124 | + makeResult(0, ScriptStatus.FAILED_INSTALLING), |
125 | + makeResult(0, ScriptStatus.TIMEDOUT), |
126 | + makeResult(0, ScriptStatus.PASSED) |
127 | + ]; |
128 | + |
129 | + expect($scope.isSuppressible(results[0])).toBe(true); |
130 | + expect($scope.isSuppressible(results[1])).toBe(true); |
131 | + expect($scope.isSuppressible(results[2])).toBe(true); |
132 | + expect($scope.isSuppressible(results[3])).toBe(false); |
133 | + }); |
134 | + }); |
135 | + |
136 | + describe("getSuppressedCount", () => { |
137 | + it("returns number of suppressed tests in node test results", () => { |
138 | + makeController(); |
139 | + const results1 = Array.from(Array(5)).map((e, i) => { |
140 | + const result = makeResult(1, ScriptStatus.FAILED); |
141 | + if (i % 2 === 0) { |
142 | + result.suppressed = true; |
143 | + } |
144 | + return result; |
145 | + }); |
146 | + const results2 = Array.from(Array(5)).map((e, i) => { |
147 | + const result = makeResult(2, ScriptStatus.FAILED); |
148 | + if (i % 2 === 0) { |
149 | + result.suppressed = true; |
150 | + } |
151 | + return result; |
152 | + }); |
153 | + $scope.results = [ |
154 | + { |
155 | + hardware_type: 1, |
156 | + results: { |
157 | + subtype: results1 |
158 | + } |
159 | + }, |
160 | + { |
161 | + hardware_type: 2, |
162 | + results: { |
163 | + subtype: results2 |
164 | + } |
165 | + } |
166 | + ]; |
167 | + |
168 | + expect($scope.getSuppressedCount()).toEqual(6); |
169 | + }); |
170 | + |
171 | + it("returns 'All' if all suppressible tests are suppressed", () => { |
172 | + makeController(); |
173 | + const results = Array.from(Array(5)).map((e, i) => { |
174 | + const result = makeResult(2, ScriptStatus.FAILED); |
175 | + result.suppressed = true; |
176 | + return result; |
177 | + }); |
178 | + $scope.results = [ |
179 | + { |
180 | + hardware_type: 1, |
181 | + results: { |
182 | + subtype: results |
183 | + } |
184 | + } |
185 | + ]; |
186 | + |
187 | + expect($scope.getSuppressedCount()).toEqual("All"); |
188 | + }); |
189 | + }); |
190 | + |
191 | + describe("toggleSuppressed", () => { |
192 | + it("calls suppressTests manager method if test not suppressed", () => { |
193 | + makeController(); |
194 | + const result = makeResult(); |
195 | + $scope.node = node; |
196 | + spyOn($scope.nodesManager, "suppressTests"); |
197 | + |
198 | + $scope.toggleSuppressed(result); |
199 | + |
200 | + expect($scope.nodesManager.suppressTests).toHaveBeenCalledWith( |
201 | + $scope.node, |
202 | + [result] |
203 | + ); |
204 | + }); |
205 | + |
206 | + it("calls unsuppressTests manager method if test suppressed", () => { |
207 | + makeController(); |
208 | + const result = makeResult(); |
209 | + result.suppressed = true; |
210 | + $scope.node = node; |
211 | + spyOn($scope.nodesManager, "unsuppressTests"); |
212 | + |
213 | + $scope.toggleSuppressed(result); |
214 | + |
215 | + expect($scope.nodesManager.unsuppressTests).toHaveBeenCalledWith( |
216 | + $scope.node, |
217 | + [result] |
218 | + ); |
219 | + }); |
220 | + }); |
221 | }); |
222 | diff --git a/src/maasserver/static/js/angular/directives/machines_table.js b/src/maasserver/static/js/angular/directives/machines_table.js |
223 | index eed1f01..56666d2 100644 |
224 | --- a/src/maasserver/static/js/angular/directives/machines_table.js |
225 | +++ b/src/maasserver/static/js/angular/directives/machines_table.js |
226 | @@ -6,7 +6,7 @@ |
227 | * Renders the machines listing. |
228 | */ |
229 | |
230 | -import NodeStatus from "../enum"; |
231 | +import { NodeStatus } from "../enum"; |
232 | |
233 | /* @ngInject */ |
234 | function maasMachinesTable( |
235 | diff --git a/src/maasserver/static/js/angular/enum.js b/src/maasserver/static/js/angular/enum.js |
236 | index d791cb6..4502c76 100644 |
237 | --- a/src/maasserver/static/js/angular/enum.js |
238 | +++ b/src/maasserver/static/js/angular/enum.js |
239 | @@ -1,4 +1,4 @@ |
240 | -const NodeStatus = { |
241 | +export const NodeStatus = { |
242 | // The node has been created and has a system ID assigned to it. |
243 | NEW: 0, |
244 | // Testing and other commissioning steps are taking place. |
245 | @@ -50,4 +50,15 @@ const NodeStatus = { |
246 | FAILED_TESTING: 22 |
247 | }; |
248 | |
249 | -export default NodeStatus; |
250 | +export const ScriptStatus = { |
251 | + PENDING: 0, |
252 | + RUNNING: 1, |
253 | + PASSED: 2, |
254 | + FAILED: 3, |
255 | + TIMEDOUT: 4, |
256 | + ABORTED: 5, |
257 | + DEGRADED: 6, |
258 | + INSTALLING: 7, |
259 | + FAILED_INSTALLING: 8, |
260 | + SKIPPED: 9 |
261 | +}; |
262 | diff --git a/src/maasserver/static/js/angular/factories/nodes.js b/src/maasserver/static/js/angular/factories/nodes.js |
263 | index 5e812b7..a098c7a 100644 |
264 | --- a/src/maasserver/static/js/angular/factories/nodes.js |
265 | +++ b/src/maasserver/static/js/angular/factories/nodes.js |
266 | @@ -382,6 +382,26 @@ function NodesManager(RegionConnection, Manager, KVMDeployOSBlacklist) { |
267 | return true; |
268 | }; |
269 | |
270 | + NodesManager.prototype.suppressTests = function(node, scripts) { |
271 | + return RegionConnection.callMethod( |
272 | + this._handler + ".set_script_result_suppressed", |
273 | + { |
274 | + system_id: node.system_id, |
275 | + script_result_ids: scripts.map(script => script.id) |
276 | + } |
277 | + ); |
278 | + }; |
279 | + |
280 | + NodesManager.prototype.unsuppressTests = function(node, scripts) { |
281 | + return RegionConnection.callMethod( |
282 | + this._handler + ".set_script_result_unsuppressed", |
283 | + { |
284 | + system_id: node.system_id, |
285 | + script_result_ids: scripts.map(script => script.id) |
286 | + } |
287 | + ); |
288 | + }; |
289 | + |
290 | return NodesManager; |
291 | } |
292 | |
293 | diff --git a/src/maasserver/static/js/angular/factories/tests/test_nodes.js b/src/maasserver/static/js/angular/factories/tests/test_nodes.js |
294 | index c74a9e4..b0b067d 100644 |
295 | --- a/src/maasserver/static/js/angular/factories/tests/test_nodes.js |
296 | +++ b/src/maasserver/static/js/angular/factories/tests/test_nodes.js |
297 | @@ -797,4 +797,46 @@ describe("NodesManager", function() { |
298 | }); |
299 | }); |
300 | }); |
301 | + |
302 | + describe("suppressTests", () => { |
303 | + it("calls machine.set_script_result_suppressed for script list", done => { |
304 | + const machine = makemachine(); |
305 | + const scripts = [{ id: makeName("id") }, { id: makeName("id") }]; |
306 | + |
307 | + webSocket.returnData.push(makeFakeResponse("updated")); |
308 | + |
309 | + MachinesManager.suppressTests(machine, scripts).then(() => { |
310 | + const sentObject = angular.fromJson(webSocket.sentData[0]); |
311 | + expect(sentObject.method).toBe("machine.set_script_result_suppressed"); |
312 | + expect(sentObject.params.system_id).toBe(machine.system_id); |
313 | + expect(sentObject.params.script_result_ids).toEqual([ |
314 | + scripts[0].id, |
315 | + scripts[1].id |
316 | + ]); |
317 | + done(); |
318 | + }); |
319 | + }); |
320 | + }); |
321 | + |
322 | + describe("unsuppressTests", () => { |
323 | + it("calls machine.set_script_result_unsuppressed for script list", done => { |
324 | + const machine = makemachine(); |
325 | + const scripts = [{ id: makeName("id") }, { id: makeName("id") }]; |
326 | + |
327 | + webSocket.returnData.push(makeFakeResponse("updated")); |
328 | + |
329 | + MachinesManager.unsuppressTests(machine, scripts).then(() => { |
330 | + const sentObject = angular.fromJson(webSocket.sentData[0]); |
331 | + expect(sentObject.method).toBe( |
332 | + "machine.set_script_result_unsuppressed" |
333 | + ); |
334 | + expect(sentObject.params.system_id).toBe(machine.system_id); |
335 | + expect(sentObject.params.script_result_ids).toEqual([ |
336 | + scripts[0].id, |
337 | + scripts[1].id |
338 | + ]); |
339 | + done(); |
340 | + }); |
341 | + }); |
342 | + }); |
343 | }); |
344 | diff --git a/src/maasserver/static/partials/script-results-list.html b/src/maasserver/static/partials/script-results-list.html |
345 | index 0246116..f968d48 100644 |
346 | --- a/src/maasserver/static/partials/script-results-list.html |
347 | +++ b/src/maasserver/static/partials/script-results-list.html |
348 | @@ -4,6 +4,16 @@ |
349 | <p class="u-text--loading"><i class="p-icon--spinner u-animation--spin"></i> Loading...</p> |
350 | </div> |
351 | </div> |
352 | + <div class="row" data-ng-if="resultsLoaded && hasSuppressedTests()"> |
353 | + <div class="p-notification--caution"> |
354 | + <p class="p-notification__response"> |
355 | + <span class="p-notification__status"> |
356 | + {$ getSuppressedCount() $} test failure{$ getSuppressedCount() === 1 ? "" : "s" $} suppressed: |
357 | + </span> |
358 | + <span>suppressed results are not shown on the <em>Machine list</em>, but are always shown here.</span> |
359 | + </p> |
360 | + </div> |
361 | + </div> |
362 | <div class="row"> |
363 | <div data-ng-repeat="hardware_type in results"> |
364 | <div data-ng-if="resultsLoaded && (hardware_type.results | json) != '{}'"> |
365 | @@ -13,6 +23,7 @@ |
366 | <table class="p-table-expanding p-table--controllers-commissioning"> |
367 | <thead> |
368 | <tr> |
369 | + <th style="width: 80px">Suppress</th> |
370 | <th class="col-3">Name</th> |
371 | <th class="col-2">Tags</th> |
372 | <th class="col-1">Runtime</th> |
373 | @@ -22,7 +33,27 @@ |
374 | </tr> |
375 | </thead> |
376 | <tbody> |
377 | - <tr data-ng-repeat="result in results" data-ng-class="{'is-active': result.showing_results || result.showing_history}"> |
378 | + <tr |
379 | + data-ng-repeat="result in results" |
380 | + data-ng-class="{ |
381 | + 'is-active': result.showing_results || result.showing_history, |
382 | + 'is-suppressed': result.suppressed |
383 | + }" |
384 | + > |
385 | + <td style="width: 80px; overflow: visible" aria-label="Suppress""> |
386 | + <div class="p-tooltip"> |
387 | + <input |
388 | + id="{$ result.id $}" |
389 | + type="checkbox" |
390 | + data-ng-if="isSuppressible(result)" |
391 | + data-ng-checked="result.suppressed" |
392 | + data-ng-click="toggleSuppressed(result)" |
393 | + data-ng-disabled="!isSuperUser()" |
394 | + > |
395 | + <label style="height: 2rem" for="{$ result.id $}" data-ng-if="result.exit_status !== 0"></label> |
396 | + <span style="left: -1rem" data-ng-if="!isSuperUser()" class="p-tooltip__message">Only admins can suppress failed tests.</span> |
397 | + </div> |
398 | + </td> |
399 | <td class="col-3" data-ng-click="result.showing_results = !result.showing_results" aria-label="Name" title="{$ result.name $}"> |
400 | <span data-maas-script-status="script-status" data-script-status="result.status"></span> |
401 | {$ result.name $} |
402 | @@ -42,11 +73,12 @@ |
403 | <button class="p-button--base is-small p-contextual-menu__toggle" data-ng-click="toggleMenu()"> |
404 | <i class="p-icon--contextual-menu u-no-margin--right">Actions</i> |
405 | </button> |
406 | - <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled"> |
407 | + <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled" style="width: auto"> |
408 | <button class="p-contextual-menu__link" aria-label="View metrics" data-ng-if="!result.showing_results && result.results.length !== 0" data-ng-click="toggleMenu(); result.showing_history = false; result.showing_results = true">View metrics</button> |
409 | <button class="p-contextual-menu__link" aria-label="Hide metrics" data-ng-if="result.showing_results" data-ng-click="toggleMenu(); result.showing_history = false; result.showing_results = false">Hide metrics</button> |
410 | <button class="p-contextual-menu__link" aria-label="View previous {$ result.result_section $}" data-ng-if="!result.showing_history" data-ng-click="toggleMenu(); loadHistory(result)">View previous {$ result.result_section $}</button> |
411 | <button class="p-contextual-menu__link" aria-label="Hide previous {$ result.result_section $}" data-ng-if="result.showing_history" data-ng-click="toggleMenu(); result.showing_results = false; result.showing_history = false">Hide previous {$ result.result_section $}</button> |
412 | + <button class="p-contextual-menu__link" aria-label="Toggle this test in machine list" data-ng-if="isSuperUser() && isSuppressible(result)" data-ng-click="toggleSuppressed(result)">{$ result.suppressed ? "Show" : "Suppress" $} test in machine list</button> |
413 | </div> |
414 | </div> |
415 | </div> |
416 | diff --git a/src/maasserver/static/scss/_base_tables.scss b/src/maasserver/static/scss/_base_tables.scss |
417 | index 657439e..710e416 100644 |
418 | --- a/src/maasserver/static/scss/_base_tables.scss |
419 | +++ b/src/maasserver/static/scss/_base_tables.scss |
420 | @@ -61,6 +61,16 @@ |
421 | background-color: $color-x-light; |
422 | } |
423 | |
424 | + &.is-suppressed { |
425 | + > td:nth-child(2), |
426 | + > td:nth-child(3), |
427 | + > td:nth-child(4), |
428 | + > td:nth-child(5), |
429 | + > td:nth-child(6) { |
430 | + opacity: 0.5; |
431 | + } |
432 | + } |
433 | + |
434 | thead & { |
435 | border-bottom-color: $color-mid-light; |
436 | } |
437 | diff --git a/src/maasserver/websockets/handlers/controller.py b/src/maasserver/websockets/handlers/controller.py |
438 | index 9acd874..609c5e9 100644 |
439 | --- a/src/maasserver/websockets/handlers/controller.py |
440 | +++ b/src/maasserver/websockets/handlers/controller.py |
441 | @@ -73,6 +73,7 @@ class ControllerHandler(MachineHandler): |
442 | 'get_summary_xml', |
443 | 'get_summary_yaml', |
444 | 'set_script_result_suppressed', |
445 | + 'set_script_result_unsuppressed', |
446 | 'get_suppressible_script_results', |
447 | ] |
448 | form = ControllerForm |
449 | diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py |
450 | index a6469d8..ebba122 100644 |
451 | --- a/src/maasserver/websockets/handlers/machine.py |
452 | +++ b/src/maasserver/websockets/handlers/machine.py |
453 | @@ -196,6 +196,7 @@ class MachineHandler(NodeHandler): |
454 | 'get_summary_xml', |
455 | 'get_summary_yaml', |
456 | 'set_script_result_suppressed', |
457 | + 'set_script_result_unsuppressed', |
458 | 'get_suppressible_script_results', |
459 | ] |
460 | form = AdminMachineWithMACAddressesForm |
461 | diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py |
462 | index c344577..bf93dc8 100644 |
463 | --- a/src/maasserver/websockets/handlers/node.py |
464 | +++ b/src/maasserver/websockets/handlers/node.py |
465 | @@ -34,6 +34,7 @@ from maasserver.models.physicalblockdevice import PhysicalBlockDevice |
466 | from maasserver.models.tag import Tag |
467 | from maasserver.models.virtualblockdevice import VirtualBlockDevice |
468 | from maasserver.node_action import compile_node_actions |
469 | +from maasserver.permissions import NodePermission |
470 | from maasserver.storage_layouts import get_applied_storage_layout_for_node |
471 | from maasserver.third_party_drivers import get_third_party_driver |
472 | from maasserver.utils.converters import ( |
473 | @@ -44,6 +45,7 @@ from maasserver.utils.osystems import make_hwe_kernel_ui_text |
474 | from maasserver.websockets.base import ( |
475 | dehydrate_datetime, |
476 | HandlerError, |
477 | + HandlerPermissionError, |
478 | ) |
479 | from maasserver.websockets.handlers.event import dehydrate_event_type_level |
480 | from maasserver.websockets.handlers.node_result import NodeResultHandler |
481 | @@ -897,10 +899,22 @@ class NodeHandler(TimestampedModelHandler): |
482 | |
483 | def set_script_result_suppressed(self, params): |
484 | """Set suppressed for the ScriptResult ids.""" |
485 | + node = self.get_object(params) |
486 | + if not self.user.has_perm(NodePermission.admin, node): |
487 | + raise HandlerPermissionError() |
488 | script_result_ids = params.get('script_result_ids') |
489 | ScriptResult.objects.filter(id__in=script_result_ids).update( |
490 | suppressed=True) |
491 | |
492 | + def set_script_result_unsuppressed(self, params): |
493 | + """Set unsuppressed for the ScriptResult ids.""" |
494 | + node = self.get_object(params) |
495 | + if not self.user.has_perm(NodePermission.admin, node): |
496 | + raise HandlerPermissionError() |
497 | + script_result_ids = params.get('script_result_ids') |
498 | + ScriptResult.objects.filter(id__in=script_result_ids).update( |
499 | + suppressed=False) |
500 | + |
501 | def get_suppressible_script_results(self, params): |
502 | """Return a dictionary with Nodes system_ids mapped to lists of |
503 | ScriptResults that can still be suppressed.""" |
504 | diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py |
505 | index 5257e2a..916a8b1 100644 |
506 | --- a/src/maasserver/websockets/handlers/tests/test_machine.py |
507 | +++ b/src/maasserver/websockets/handlers/tests/test_machine.py |
508 | @@ -4286,7 +4286,7 @@ class TestMachineHandlerUpdateFilesystem(MAASServerTestCase): |
509 | self.assertEqual(blockdevice.tags, []) |
510 | |
511 | def test__set_script_result_suppressed(self): |
512 | - owner = factory.make_User() |
513 | + owner = factory.make_admin() |
514 | node = factory.make_Node(owner=owner) |
515 | handler = MachineHandler(owner, {}, None) |
516 | hardware_type = factory.pick_choice(HARDWARE_TYPE_CHOICES) |
517 | @@ -4310,6 +4310,72 @@ class TestMachineHandlerUpdateFilesystem(MAASServerTestCase): |
518 | script_result = reload_object(script_result) |
519 | self.assertTrue(script_result.suppressed) |
520 | |
521 | + def test__set_script_result_suppressed_raises_permission_error(self): |
522 | + owner = factory.make_User() |
523 | + node = factory.make_Node(owner=owner) |
524 | + handler = MachineHandler(owner, {}, None) |
525 | + script_set = factory.make_ScriptSet(node=node) |
526 | + script_results = [] |
527 | + for _ in range(3): |
528 | + script_results.append( |
529 | + factory.make_ScriptResult(script_set=script_set)) |
530 | + params = { |
531 | + 'system_id': node.system_id, |
532 | + 'script_result_ids': [ |
533 | + script_result.id for script_result in script_results] |
534 | + } |
535 | + |
536 | + self.assertRaises( |
537 | + HandlerPermissionError, |
538 | + handler.set_script_result_suppressed, |
539 | + params) |
540 | + |
541 | + def test__set_script_result_unsuppressed(self): |
542 | + owner = factory.make_admin() |
543 | + node = factory.make_Node(owner=owner) |
544 | + handler = MachineHandler(owner, {}, None) |
545 | + hardware_type = factory.pick_choice(HARDWARE_TYPE_CHOICES) |
546 | + script_set = factory.make_ScriptSet(node=node) |
547 | + script = factory.make_Script(hardware_type=hardware_type) |
548 | + script_result = factory.make_ScriptResult( |
549 | + script_set, script=script, suppressed=True) |
550 | + |
551 | + script_results = [] |
552 | + for _ in range(3): |
553 | + script_results.append( |
554 | + factory.make_ScriptResult(script_set=script_set)) |
555 | + |
556 | + handler.set_script_result_unsuppressed({ |
557 | + 'system_id': node.system_id, |
558 | + 'script_result_ids': [ |
559 | + script_result.id for script_result in script_results] |
560 | + }) |
561 | + script_result = reload_object(script_result) |
562 | + self.assertTrue(script_result.suppressed) |
563 | + for script_result in script_results: |
564 | + script_result = reload_object(script_result) |
565 | + self.assertFalse(script_result.suppressed) |
566 | + |
567 | + def test__set_script_result_unsuppressed_raises_permission_error(self): |
568 | + owner = factory.make_User() |
569 | + node = factory.make_Node(owner=owner) |
570 | + handler = MachineHandler(owner, {}, None) |
571 | + script_set = factory.make_ScriptSet(node=node) |
572 | + script_results = [] |
573 | + for _ in range(3): |
574 | + script_results.append( |
575 | + factory.make_ScriptResult(script_set=script_set)) |
576 | + params = { |
577 | + 'system_id': node.system_id, |
578 | + 'script_result_ids': [ |
579 | + script_result.id for script_result in script_results] |
580 | + } |
581 | + |
582 | + self.assertRaises( |
583 | + HandlerPermissionError, |
584 | + handler.set_script_result_unsuppressed, |
585 | + params) |
586 | + |
587 | def test__get_unsuppressed_script_results(self): |
588 | owner = factory.make_User() |
589 | handler = MachineHandler(owner, {}, None) |
UI looks good, just some things that need to be fixed.