Merge ~caleb-ellis/maas:suppress-single-test into maas:master

Proposed by Caleb Ellis
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)
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://app.zeplin.io/project/5bfff7810f5b0001ebd72a27/dashboard

## 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://user-images.githubusercontent.com/25733845/56738843-5d870b80-6765-11e9-8a37-531ee62efb70.png

### One test suppressed
https://user-images.githubusercontent.com/25733845/56738848-61b32900-6765-11e9-9255-3fa57818da5c.png

### All tests suppressed
https://user-images.githubusercontent.com/25733845/56738879-77c0e980-6765-11e9-999a-73eea6e11a10.png

### Updated summary page
https://user-images.githubusercontent.com/25733845/56738887-7c859d80-6765-11e9-9ce5-de3325b37795.png

### Updated listing page
https://user-images.githubusercontent.com/25733845/56738901-827b7e80-6765-11e9-91e5-af00dcac0eb5.png

EDIT: Updated to have an "All" label and made the Hide col a bit thinner:
https://user-images.githubusercontent.com/25733845/56890058-42234580-6a70-11e9-9219-23b6a8c4e72f.png

EDIT2: Updated so only admins can suppress/unsuppress tests. If user is a non-admin, the checkbox is disabled with a tooltip:
https://user-images.githubusercontent.com/25733845/56906850-fb971080-6a9a-11e9-91af-6f381d58b333.png

EDIT3: Added ScriptStatus to enum.js, and updated so only tests that have FAILED, FAILED_INSTALLING or TIMEDOUT can be suppressed.
https://user-images.githubusercontent.com/25733845/56949538-1a44e800-6b2b-11e9-9dfa-6042a6def50c.png

EDIT4: Changed instances of "hide" to "suppress":
https://user-images.githubusercontent.com/25733845/56979857-a4fd0580-6b72-11e9-9b49-d9742ab0c219.png

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

UI looks good, just some things that need to be fixed.

review: Needs Fixing
Revision history for this message
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?

Revision history for this message
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_result_suppressed which is the inverse of set_script_result_unsuppressed that Cabel implemented. I think we should fix both in a separate bug. However the UI should be updated to also send the system_id.

review: Needs Fixing
Revision history for this message
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_result_suppressed which is the inverse of
> set_script_result_unsuppressed that Cabel implemented. I think we should
> fix both in a separate bug. However the UI should be updated to also send
> the system_id.
> --
> https://code.launchpad.net/~caleb-ellis/maas/+git/maas/+merge/366513
> Your team MAAS UI team is requested to review the proposed merge of
> ~caleb-ellis/maas:suppress-single-test into maas:master.
>

Revision history for this message
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_result_suppressed which is the inverse of
> set_script_result_unsuppressed that Cabel implemented. I think we should fix
> 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?

Revision history for this message
Blake Rouse (blake-rouse) :
review: Needs Fixing
Revision history for this message
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.

Revision history for this message
Lilyana Videnova (lilyanavidenova) wrote :

Can we add a tooltip on those disabled checkboxes saying "Only admins can hide failed tests."

Revision history for this message
Caleb Ellis (caleb-ellis) wrote :
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Thanks for the fixes, looks good.

review: Approve
Revision history for this message
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.

review: Needs Fixing
Revision history for this message
Anthony Dillon (ya-bo-ng) wrote :

Code looks good to me +1, so inline styles but nothing alarming.

review: Approve
Revision history for this message
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://app.zeplin.io/project/5bfff7810f5b0001ebd72a27). If it proves to be confusing for users it's only a simple text change.

https://user-images.githubusercontent.com/25733845/56949538-1a44e800-6b2b-11e9-9dfa-6042a6def50c.png

Revision history for this message
Blake Rouse (blake-rouse) :
Revision history for this message
Caleb Ellis (caleb-ellis) :
Revision history for this message
Blake Rouse (blake-rouse) :
Revision history for this message
Lee Trager (ltrager) wrote :

My bad I did make javascript instead of make force-javascript. Thanks for the fixes, LGTM!

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
2b0be59... by Caleb Ellis

Fix linting

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/static/js/angular/controllers/node_results.js b/src/maasserver/static/js/angular/controllers/node_results.js
2index 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.
58diff --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
59index 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 });
222diff --git a/src/maasserver/static/js/angular/directives/machines_table.js b/src/maasserver/static/js/angular/directives/machines_table.js
223index 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(
235diff --git a/src/maasserver/static/js/angular/enum.js b/src/maasserver/static/js/angular/enum.js
236index 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+};
262diff --git a/src/maasserver/static/js/angular/factories/nodes.js b/src/maasserver/static/js/angular/factories/nodes.js
263index 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
293diff --git a/src/maasserver/static/js/angular/factories/tests/test_nodes.js b/src/maasserver/static/js/angular/factories/tests/test_nodes.js
294index 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 });
344diff --git a/src/maasserver/static/partials/script-results-list.html b/src/maasserver/static/partials/script-results-list.html
345index 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>&nbsp;&nbsp;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>
416diff --git a/src/maasserver/static/scss/_base_tables.scss b/src/maasserver/static/scss/_base_tables.scss
417index 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 }
437diff --git a/src/maasserver/websockets/handlers/controller.py b/src/maasserver/websockets/handlers/controller.py
438index 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
449diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py
450index 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
461diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py
462index 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."""
504diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py
505index 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)

Subscribers

People subscribed via source and target branches