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
diff --git a/src/maasserver/static/js/angular/controllers/node_results.js b/src/maasserver/static/js/angular/controllers/node_results.js
index 66a7c0d..6ebccb9 100644
--- a/src/maasserver/static/js/angular/controllers/node_results.js
+++ b/src/maasserver/static/js/angular/controllers/node_results.js
@@ -1,4 +1,4 @@
1import NodeResultController from "./node_result";1import { ScriptStatus } from "../enum";
22
3/* Copyright 2017-2018 Canonical Ltd. This software is licensed under the3/* Copyright 2017-2018 Canonical Ltd. This software is licensed under the
4 * GNU Affero General Public License version 3 (see the file LICENSE).4 * GNU Affero General Public License version 3 (see the file LICENSE).
@@ -222,6 +222,46 @@ function NodeResultsController(
222 });222 });
223 };223 };
224224
225 $scope.hasSuppressedTests = () => {
226 return $scope.results.some(type => {
227 const entries = Object.entries(type.results);
228 return entries.some(entry => entry[1].some(result => result.suppressed));
229 });
230 };
231
232 $scope.isSuppressible = result =>
233 result.status === ScriptStatus.FAILED ||
234 result.status === ScriptStatus.FAILED_INSTALLING ||
235 result.status === ScriptStatus.TIMEDOUT;
236
237 $scope.getSuppressedCount = () => {
238 const suppressibleTests = $scope.results.reduce((acc, type) => {
239 const entries = Object.entries(type.results);
240 entries.forEach(entry => {
241 entry[1].forEach(result => {
242 if ($scope.isSuppressible(result)) {
243 acc.push(result);
244 }
245 });
246 });
247 return acc;
248 }, []);
249 const suppressedTests = suppressibleTests.filter(test => test.suppressed);
250
251 if (suppressibleTests.length === suppressedTests.length) {
252 return "All";
253 }
254 return suppressedTests.length;
255 };
256
257 $scope.toggleSuppressed = result => {
258 if (result.suppressed) {
259 $scope.nodesManager.unsuppressTests($scope.node, [result]);
260 } else {
261 $scope.nodesManager.suppressTests($scope.node, [result]);
262 }
263 };
264
225 // Destroy the NodeResultsManager when the scope is destroyed. This is265 // Destroy the NodeResultsManager when the scope is destroyed. This is
226 // so the client will not recieve any more notifications about results266 // so the client will not recieve any more notifications about results
227 // from this node.267 // from this node.
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
index 1839b07..c0712f2 100644
--- a/src/maasserver/static/js/angular/controllers/tests/test_node_results.js
+++ b/src/maasserver/static/js/angular/controllers/tests/test_node_results.js
@@ -4,6 +4,21 @@
4 * Unit tests for NodeResultsController4 * Unit tests for NodeResultsController
5 */5 */
66
7// 2019-04-30 Caleb - Syntax error `import { ScriptStatus }from "../../enum"`;
8// TODO - Fix es module imports in test files
9const ScriptStatus = {
10 PENDING: 0,
11 RUNNING: 1,
12 PASSED: 2,
13 FAILED: 3,
14 TIMEDOUT: 4,
15 ABORTED: 5,
16 DEGRADED: 6,
17 INSTALLING: 7,
18 FAILED_INSTALLING: 8,
19 SKIPPED: 9
20};
21
7describe("NodeResultsController", function() {22describe("NodeResultsController", function() {
8 // Load the MAAS module.23 // Load the MAAS module.
9 beforeEach(module("MAAS"));24 beforeEach(module("MAAS"));
@@ -511,4 +526,137 @@ describe("NodeResultsController", function() {
511 expect($scope.nodeResultsManager.get_history).not.toHaveBeenCalled();526 expect($scope.nodeResultsManager.get_history).not.toHaveBeenCalled();
512 });527 });
513 });528 });
529
530 describe("hasSuppressedTests", () => {
531 it("returns whether there are suppressed tests in results", () => {
532 makeController();
533 const suppressedResult = makeResult(1);
534 $scope.results = [
535 {
536 hardware_type: 1,
537 results: {
538 subtype: [
539 ...Array.from(Array(3)).map(() => makeResult(1)),
540 suppressedResult
541 ]
542 }
543 },
544 {
545 hardware_type: 2,
546 results: {
547 subtype: Array.from(Array(3)).map(() => makeResult(2))
548 }
549 }
550 ];
551
552 expect($scope.hasSuppressedTests()).toEqual(false);
553
554 suppressedResult.suppressed = true;
555 expect($scope.hasSuppressedTests()).toEqual(true);
556 });
557 });
558
559 describe("isSuppressible", () => {
560 it(`returns true if a result's status is
561 FAILED, FAILED_INSTALLING or TIMEDOUT`, () => {
562 makeController();
563 const results = [
564 makeResult(0, ScriptStatus.FAILED),
565 makeResult(0, ScriptStatus.FAILED_INSTALLING),
566 makeResult(0, ScriptStatus.TIMEDOUT),
567 makeResult(0, ScriptStatus.PASSED)
568 ];
569
570 expect($scope.isSuppressible(results[0])).toBe(true);
571 expect($scope.isSuppressible(results[1])).toBe(true);
572 expect($scope.isSuppressible(results[2])).toBe(true);
573 expect($scope.isSuppressible(results[3])).toBe(false);
574 });
575 });
576
577 describe("getSuppressedCount", () => {
578 it("returns number of suppressed tests in node test results", () => {
579 makeController();
580 const results1 = Array.from(Array(5)).map((e, i) => {
581 const result = makeResult(1, ScriptStatus.FAILED);
582 if (i % 2 === 0) {
583 result.suppressed = true;
584 }
585 return result;
586 });
587 const results2 = Array.from(Array(5)).map((e, i) => {
588 const result = makeResult(2, ScriptStatus.FAILED);
589 if (i % 2 === 0) {
590 result.suppressed = true;
591 }
592 return result;
593 });
594 $scope.results = [
595 {
596 hardware_type: 1,
597 results: {
598 subtype: results1
599 }
600 },
601 {
602 hardware_type: 2,
603 results: {
604 subtype: results2
605 }
606 }
607 ];
608
609 expect($scope.getSuppressedCount()).toEqual(6);
610 });
611
612 it("returns 'All' if all suppressible tests are suppressed", () => {
613 makeController();
614 const results = Array.from(Array(5)).map((e, i) => {
615 const result = makeResult(2, ScriptStatus.FAILED);
616 result.suppressed = true;
617 return result;
618 });
619 $scope.results = [
620 {
621 hardware_type: 1,
622 results: {
623 subtype: results
624 }
625 }
626 ];
627
628 expect($scope.getSuppressedCount()).toEqual("All");
629 });
630 });
631
632 describe("toggleSuppressed", () => {
633 it("calls suppressTests manager method if test not suppressed", () => {
634 makeController();
635 const result = makeResult();
636 $scope.node = node;
637 spyOn($scope.nodesManager, "suppressTests");
638
639 $scope.toggleSuppressed(result);
640
641 expect($scope.nodesManager.suppressTests).toHaveBeenCalledWith(
642 $scope.node,
643 [result]
644 );
645 });
646
647 it("calls unsuppressTests manager method if test suppressed", () => {
648 makeController();
649 const result = makeResult();
650 result.suppressed = true;
651 $scope.node = node;
652 spyOn($scope.nodesManager, "unsuppressTests");
653
654 $scope.toggleSuppressed(result);
655
656 expect($scope.nodesManager.unsuppressTests).toHaveBeenCalledWith(
657 $scope.node,
658 [result]
659 );
660 });
661 });
514});662});
diff --git a/src/maasserver/static/js/angular/directives/machines_table.js b/src/maasserver/static/js/angular/directives/machines_table.js
index eed1f01..56666d2 100644
--- a/src/maasserver/static/js/angular/directives/machines_table.js
+++ b/src/maasserver/static/js/angular/directives/machines_table.js
@@ -6,7 +6,7 @@
6 * Renders the machines listing.6 * Renders the machines listing.
7 */7 */
88
9import NodeStatus from "../enum";9import { NodeStatus } from "../enum";
1010
11/* @ngInject */11/* @ngInject */
12function maasMachinesTable(12function maasMachinesTable(
diff --git a/src/maasserver/static/js/angular/enum.js b/src/maasserver/static/js/angular/enum.js
index d791cb6..4502c76 100644
--- a/src/maasserver/static/js/angular/enum.js
+++ b/src/maasserver/static/js/angular/enum.js
@@ -1,4 +1,4 @@
1const NodeStatus = {1export const NodeStatus = {
2 // The node has been created and has a system ID assigned to it.2 // The node has been created and has a system ID assigned to it.
3 NEW: 0,3 NEW: 0,
4 // Testing and other commissioning steps are taking place.4 // Testing and other commissioning steps are taking place.
@@ -50,4 +50,15 @@ const NodeStatus = {
50 FAILED_TESTING: 2250 FAILED_TESTING: 22
51};51};
5252
53export default NodeStatus;53export const ScriptStatus = {
54 PENDING: 0,
55 RUNNING: 1,
56 PASSED: 2,
57 FAILED: 3,
58 TIMEDOUT: 4,
59 ABORTED: 5,
60 DEGRADED: 6,
61 INSTALLING: 7,
62 FAILED_INSTALLING: 8,
63 SKIPPED: 9
64};
diff --git a/src/maasserver/static/js/angular/factories/nodes.js b/src/maasserver/static/js/angular/factories/nodes.js
index 5e812b7..a098c7a 100644
--- a/src/maasserver/static/js/angular/factories/nodes.js
+++ b/src/maasserver/static/js/angular/factories/nodes.js
@@ -382,6 +382,26 @@ function NodesManager(RegionConnection, Manager, KVMDeployOSBlacklist) {
382 return true;382 return true;
383 };383 };
384384
385 NodesManager.prototype.suppressTests = function(node, scripts) {
386 return RegionConnection.callMethod(
387 this._handler + ".set_script_result_suppressed",
388 {
389 system_id: node.system_id,
390 script_result_ids: scripts.map(script => script.id)
391 }
392 );
393 };
394
395 NodesManager.prototype.unsuppressTests = function(node, scripts) {
396 return RegionConnection.callMethod(
397 this._handler + ".set_script_result_unsuppressed",
398 {
399 system_id: node.system_id,
400 script_result_ids: scripts.map(script => script.id)
401 }
402 );
403 };
404
385 return NodesManager;405 return NodesManager;
386}406}
387407
diff --git a/src/maasserver/static/js/angular/factories/tests/test_nodes.js b/src/maasserver/static/js/angular/factories/tests/test_nodes.js
index c74a9e4..b0b067d 100644
--- a/src/maasserver/static/js/angular/factories/tests/test_nodes.js
+++ b/src/maasserver/static/js/angular/factories/tests/test_nodes.js
@@ -797,4 +797,46 @@ describe("NodesManager", function() {
797 });797 });
798 });798 });
799 });799 });
800
801 describe("suppressTests", () => {
802 it("calls machine.set_script_result_suppressed for script list", done => {
803 const machine = makemachine();
804 const scripts = [{ id: makeName("id") }, { id: makeName("id") }];
805
806 webSocket.returnData.push(makeFakeResponse("updated"));
807
808 MachinesManager.suppressTests(machine, scripts).then(() => {
809 const sentObject = angular.fromJson(webSocket.sentData[0]);
810 expect(sentObject.method).toBe("machine.set_script_result_suppressed");
811 expect(sentObject.params.system_id).toBe(machine.system_id);
812 expect(sentObject.params.script_result_ids).toEqual([
813 scripts[0].id,
814 scripts[1].id
815 ]);
816 done();
817 });
818 });
819 });
820
821 describe("unsuppressTests", () => {
822 it("calls machine.set_script_result_unsuppressed for script list", done => {
823 const machine = makemachine();
824 const scripts = [{ id: makeName("id") }, { id: makeName("id") }];
825
826 webSocket.returnData.push(makeFakeResponse("updated"));
827
828 MachinesManager.unsuppressTests(machine, scripts).then(() => {
829 const sentObject = angular.fromJson(webSocket.sentData[0]);
830 expect(sentObject.method).toBe(
831 "machine.set_script_result_unsuppressed"
832 );
833 expect(sentObject.params.system_id).toBe(machine.system_id);
834 expect(sentObject.params.script_result_ids).toEqual([
835 scripts[0].id,
836 scripts[1].id
837 ]);
838 done();
839 });
840 });
841 });
800});842});
diff --git a/src/maasserver/static/partials/script-results-list.html b/src/maasserver/static/partials/script-results-list.html
index 0246116..f968d48 100644
--- a/src/maasserver/static/partials/script-results-list.html
+++ b/src/maasserver/static/partials/script-results-list.html
@@ -4,6 +4,16 @@
4 <p class="u-text--loading"><i class="p-icon--spinner u-animation--spin"></i>&nbsp;&nbsp;Loading...</p>4 <p class="u-text--loading"><i class="p-icon--spinner u-animation--spin"></i>&nbsp;&nbsp;Loading...</p>
5 </div>5 </div>
6 </div>6 </div>
7 <div class="row" data-ng-if="resultsLoaded && hasSuppressedTests()">
8 <div class="p-notification--caution">
9 <p class="p-notification__response">
10 <span class="p-notification__status">
11 {$ getSuppressedCount() $} test failure{$ getSuppressedCount() === 1 ? "" : "s" $} suppressed:
12 </span>
13 <span>suppressed results are not shown on the <em>Machine list</em>, but are always shown here.</span>
14 </p>
15 </div>
16 </div>
7 <div class="row">17 <div class="row">
8 <div data-ng-repeat="hardware_type in results">18 <div data-ng-repeat="hardware_type in results">
9 <div data-ng-if="resultsLoaded && (hardware_type.results | json) != '{}'">19 <div data-ng-if="resultsLoaded && (hardware_type.results | json) != '{}'">
@@ -13,6 +23,7 @@
13 <table class="p-table-expanding p-table--controllers-commissioning">23 <table class="p-table-expanding p-table--controllers-commissioning">
14 <thead>24 <thead>
15 <tr>25 <tr>
26 <th style="width: 80px">Suppress</th>
16 <th class="col-3">Name</th>27 <th class="col-3">Name</th>
17 <th class="col-2">Tags</th>28 <th class="col-2">Tags</th>
18 <th class="col-1">Runtime</th>29 <th class="col-1">Runtime</th>
@@ -22,7 +33,27 @@
22 </tr>33 </tr>
23 </thead>34 </thead>
24 <tbody>35 <tbody>
25 <tr data-ng-repeat="result in results" data-ng-class="{'is-active': result.showing_results || result.showing_history}">36 <tr
37 data-ng-repeat="result in results"
38 data-ng-class="{
39 'is-active': result.showing_results || result.showing_history,
40 'is-suppressed': result.suppressed
41 }"
42 >
43 <td style="width: 80px; overflow: visible" aria-label="Suppress"">
44 <div class="p-tooltip">
45 <input
46 id="{$ result.id $}"
47 type="checkbox"
48 data-ng-if="isSuppressible(result)"
49 data-ng-checked="result.suppressed"
50 data-ng-click="toggleSuppressed(result)"
51 data-ng-disabled="!isSuperUser()"
52 >
53 <label style="height: 2rem" for="{$ result.id $}" data-ng-if="result.exit_status !== 0"></label>
54 <span style="left: -1rem" data-ng-if="!isSuperUser()" class="p-tooltip__message">Only admins can suppress failed tests.</span>
55 </div>
56 </td>
26 <td class="col-3" data-ng-click="result.showing_results = !result.showing_results" aria-label="Name" title="{$ result.name $}">57 <td class="col-3" data-ng-click="result.showing_results = !result.showing_results" aria-label="Name" title="{$ result.name $}">
27 <span data-maas-script-status="script-status" data-script-status="result.status"></span>58 <span data-maas-script-status="script-status" data-script-status="result.status"></span>
28 {$ result.name $}59 {$ result.name $}
@@ -42,11 +73,12 @@
42 <button class="p-button--base is-small p-contextual-menu__toggle" data-ng-click="toggleMenu()">73 <button class="p-button--base is-small p-contextual-menu__toggle" data-ng-click="toggleMenu()">
43 <i class="p-icon--contextual-menu u-no-margin--right">Actions</i>74 <i class="p-icon--contextual-menu u-no-margin--right">Actions</i>
44 </button>75 </button>
45 <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled">76 <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled" style="width: auto">
46 <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>77 <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>
47 <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>78 <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>
48 <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>79 <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>
49 <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>80 <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>
81 <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>
50 </div>82 </div>
51 </div>83 </div>
52 </div>84 </div>
diff --git a/src/maasserver/static/scss/_base_tables.scss b/src/maasserver/static/scss/_base_tables.scss
index 657439e..710e416 100644
--- a/src/maasserver/static/scss/_base_tables.scss
+++ b/src/maasserver/static/scss/_base_tables.scss
@@ -61,6 +61,16 @@
61 background-color: $color-x-light;61 background-color: $color-x-light;
62 }62 }
6363
64 &.is-suppressed {
65 > td:nth-child(2),
66 > td:nth-child(3),
67 > td:nth-child(4),
68 > td:nth-child(5),
69 > td:nth-child(6) {
70 opacity: 0.5;
71 }
72 }
73
64 thead & {74 thead & {
65 border-bottom-color: $color-mid-light;75 border-bottom-color: $color-mid-light;
66 }76 }
diff --git a/src/maasserver/websockets/handlers/controller.py b/src/maasserver/websockets/handlers/controller.py
index 9acd874..609c5e9 100644
--- a/src/maasserver/websockets/handlers/controller.py
+++ b/src/maasserver/websockets/handlers/controller.py
@@ -73,6 +73,7 @@ class ControllerHandler(MachineHandler):
73 'get_summary_xml',73 'get_summary_xml',
74 'get_summary_yaml',74 'get_summary_yaml',
75 'set_script_result_suppressed',75 'set_script_result_suppressed',
76 'set_script_result_unsuppressed',
76 'get_suppressible_script_results',77 'get_suppressible_script_results',
77 ]78 ]
78 form = ControllerForm79 form = ControllerForm
diff --git a/src/maasserver/websockets/handlers/machine.py b/src/maasserver/websockets/handlers/machine.py
index a6469d8..ebba122 100644
--- a/src/maasserver/websockets/handlers/machine.py
+++ b/src/maasserver/websockets/handlers/machine.py
@@ -196,6 +196,7 @@ class MachineHandler(NodeHandler):
196 'get_summary_xml',196 'get_summary_xml',
197 'get_summary_yaml',197 'get_summary_yaml',
198 'set_script_result_suppressed',198 'set_script_result_suppressed',
199 'set_script_result_unsuppressed',
199 'get_suppressible_script_results',200 'get_suppressible_script_results',
200 ]201 ]
201 form = AdminMachineWithMACAddressesForm202 form = AdminMachineWithMACAddressesForm
diff --git a/src/maasserver/websockets/handlers/node.py b/src/maasserver/websockets/handlers/node.py
index c344577..bf93dc8 100644
--- a/src/maasserver/websockets/handlers/node.py
+++ b/src/maasserver/websockets/handlers/node.py
@@ -34,6 +34,7 @@ from maasserver.models.physicalblockdevice import PhysicalBlockDevice
34from maasserver.models.tag import Tag34from maasserver.models.tag import Tag
35from maasserver.models.virtualblockdevice import VirtualBlockDevice35from maasserver.models.virtualblockdevice import VirtualBlockDevice
36from maasserver.node_action import compile_node_actions36from maasserver.node_action import compile_node_actions
37from maasserver.permissions import NodePermission
37from maasserver.storage_layouts import get_applied_storage_layout_for_node38from maasserver.storage_layouts import get_applied_storage_layout_for_node
38from maasserver.third_party_drivers import get_third_party_driver39from maasserver.third_party_drivers import get_third_party_driver
39from maasserver.utils.converters import (40from maasserver.utils.converters import (
@@ -44,6 +45,7 @@ from maasserver.utils.osystems import make_hwe_kernel_ui_text
44from maasserver.websockets.base import (45from maasserver.websockets.base import (
45 dehydrate_datetime,46 dehydrate_datetime,
46 HandlerError,47 HandlerError,
48 HandlerPermissionError,
47)49)
48from maasserver.websockets.handlers.event import dehydrate_event_type_level50from maasserver.websockets.handlers.event import dehydrate_event_type_level
49from maasserver.websockets.handlers.node_result import NodeResultHandler51from maasserver.websockets.handlers.node_result import NodeResultHandler
@@ -897,10 +899,22 @@ class NodeHandler(TimestampedModelHandler):
897899
898 def set_script_result_suppressed(self, params):900 def set_script_result_suppressed(self, params):
899 """Set suppressed for the ScriptResult ids."""901 """Set suppressed for the ScriptResult ids."""
902 node = self.get_object(params)
903 if not self.user.has_perm(NodePermission.admin, node):
904 raise HandlerPermissionError()
900 script_result_ids = params.get('script_result_ids')905 script_result_ids = params.get('script_result_ids')
901 ScriptResult.objects.filter(id__in=script_result_ids).update(906 ScriptResult.objects.filter(id__in=script_result_ids).update(
902 suppressed=True)907 suppressed=True)
903908
909 def set_script_result_unsuppressed(self, params):
910 """Set unsuppressed for the ScriptResult ids."""
911 node = self.get_object(params)
912 if not self.user.has_perm(NodePermission.admin, node):
913 raise HandlerPermissionError()
914 script_result_ids = params.get('script_result_ids')
915 ScriptResult.objects.filter(id__in=script_result_ids).update(
916 suppressed=False)
917
904 def get_suppressible_script_results(self, params):918 def get_suppressible_script_results(self, params):
905 """Return a dictionary with Nodes system_ids mapped to lists of919 """Return a dictionary with Nodes system_ids mapped to lists of
906 ScriptResults that can still be suppressed."""920 ScriptResults that can still be suppressed."""
diff --git a/src/maasserver/websockets/handlers/tests/test_machine.py b/src/maasserver/websockets/handlers/tests/test_machine.py
index 5257e2a..916a8b1 100644
--- a/src/maasserver/websockets/handlers/tests/test_machine.py
+++ b/src/maasserver/websockets/handlers/tests/test_machine.py
@@ -4286,7 +4286,7 @@ class TestMachineHandlerUpdateFilesystem(MAASServerTestCase):
4286 self.assertEqual(blockdevice.tags, [])4286 self.assertEqual(blockdevice.tags, [])
42874287
4288 def test__set_script_result_suppressed(self):4288 def test__set_script_result_suppressed(self):
4289 owner = factory.make_User()4289 owner = factory.make_admin()
4290 node = factory.make_Node(owner=owner)4290 node = factory.make_Node(owner=owner)
4291 handler = MachineHandler(owner, {}, None)4291 handler = MachineHandler(owner, {}, None)
4292 hardware_type = factory.pick_choice(HARDWARE_TYPE_CHOICES)4292 hardware_type = factory.pick_choice(HARDWARE_TYPE_CHOICES)
@@ -4310,6 +4310,72 @@ class TestMachineHandlerUpdateFilesystem(MAASServerTestCase):
4310 script_result = reload_object(script_result)4310 script_result = reload_object(script_result)
4311 self.assertTrue(script_result.suppressed)4311 self.assertTrue(script_result.suppressed)
43124312
4313 def test__set_script_result_suppressed_raises_permission_error(self):
4314 owner = factory.make_User()
4315 node = factory.make_Node(owner=owner)
4316 handler = MachineHandler(owner, {}, None)
4317 script_set = factory.make_ScriptSet(node=node)
4318 script_results = []
4319 for _ in range(3):
4320 script_results.append(
4321 factory.make_ScriptResult(script_set=script_set))
4322 params = {
4323 'system_id': node.system_id,
4324 'script_result_ids': [
4325 script_result.id for script_result in script_results]
4326 }
4327
4328 self.assertRaises(
4329 HandlerPermissionError,
4330 handler.set_script_result_suppressed,
4331 params)
4332
4333 def test__set_script_result_unsuppressed(self):
4334 owner = factory.make_admin()
4335 node = factory.make_Node(owner=owner)
4336 handler = MachineHandler(owner, {}, None)
4337 hardware_type = factory.pick_choice(HARDWARE_TYPE_CHOICES)
4338 script_set = factory.make_ScriptSet(node=node)
4339 script = factory.make_Script(hardware_type=hardware_type)
4340 script_result = factory.make_ScriptResult(
4341 script_set, script=script, suppressed=True)
4342
4343 script_results = []
4344 for _ in range(3):
4345 script_results.append(
4346 factory.make_ScriptResult(script_set=script_set))
4347
4348 handler.set_script_result_unsuppressed({
4349 'system_id': node.system_id,
4350 'script_result_ids': [
4351 script_result.id for script_result in script_results]
4352 })
4353 script_result = reload_object(script_result)
4354 self.assertTrue(script_result.suppressed)
4355 for script_result in script_results:
4356 script_result = reload_object(script_result)
4357 self.assertFalse(script_result.suppressed)
4358
4359 def test__set_script_result_unsuppressed_raises_permission_error(self):
4360 owner = factory.make_User()
4361 node = factory.make_Node(owner=owner)
4362 handler = MachineHandler(owner, {}, None)
4363 script_set = factory.make_ScriptSet(node=node)
4364 script_results = []
4365 for _ in range(3):
4366 script_results.append(
4367 factory.make_ScriptResult(script_set=script_set))
4368 params = {
4369 'system_id': node.system_id,
4370 'script_result_ids': [
4371 script_result.id for script_result in script_results]
4372 }
4373
4374 self.assertRaises(
4375 HandlerPermissionError,
4376 handler.set_script_result_unsuppressed,
4377 params)
4378
4313 def test__get_unsuppressed_script_results(self):4379 def test__get_unsuppressed_script_results(self):
4314 owner = factory.make_User()4380 owner = factory.make_User()
4315 handler = MachineHandler(owner, {}, None)4381 handler = MachineHandler(owner, {}, None)

Subscribers

People subscribed via source and target branches