Merge lp:~blake-rouse/maas/fix-1441864 into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 3989
Proposed branch: lp:~blake-rouse/maas/fix-1441864
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~blake-rouse/maas/fix-1443959
Diff against target: 309 lines (+140/-19)
6 files modified
src/maasserver/static/js/angular/controllers/nodes_list.js (+2/-2)
src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js (+2/-2)
src/maasserver/static/js/angular/filters/nodes.js (+37/-11)
src/maasserver/static/js/angular/filters/tests/test_nodes.js (+23/-0)
src/maasserver/static/js/angular/services/search.js (+25/-4)
src/maasserver/static/js/angular/services/tests/test_search.js (+51/-0)
To merge this branch: bzr merge lp:~blake-rouse/maas/fix-1441864
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Review via email: mp+261039@code.launchpad.net

Commit message

Use exact matcher on all filters and enable the exact matcher in the nodesFilter.

This fixes issues where filtering by an item that was a subset of another item would show both matching nodes for that item.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Looks great. (I loaded this up in the dev environment and tested it for awhile.)

The only bug I found was that if I type:

status:(=allocated)

And then click the "Allocated" filter, I get:

status:(=allocated,=Allocated)

But I bet that's a pre-existing issue.

review: Approve
Revision history for this message
Blake Rouse (blake-rouse) wrote :

I fixed the status:(=allocated,=Allocated) issue thanks for pointing that out.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/static/js/angular/controllers/nodes_list.js'
2--- src/maasserver/static/js/angular/controllers/nodes_list.js 2015-06-03 20:09:46 +0000
3+++ src/maasserver/static/js/angular/controllers/nodes_list.js 2015-06-04 14:58:59 +0000
4@@ -272,7 +272,7 @@
5 $scope.toggleFilter = function(type, value, tab) {
6 leaveViewSelected(tab);
7 $scope.tabs[tab].filters = SearchService.toggleFilter(
8- $scope.tabs[tab].filters, type, value);
9+ $scope.tabs[tab].filters, type, value, true);
10 $scope.tabs[tab].search = SearchService.filtersToString(
11 $scope.tabs[tab].filters);
12 shouldClearAction(tab);
13@@ -281,7 +281,7 @@
14 // Return True if the filter is active.
15 $scope.isFilterActive = function(type, value, tab) {
16 return SearchService.isFilterActive(
17- $scope.tabs[tab].filters, type, value);
18+ $scope.tabs[tab].filters, type, value, true);
19 };
20
21 // Update the filters object when the search bar is updated.
22
23=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js'
24--- src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js 2015-06-03 20:09:46 +0000
25+++ src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js 2015-06-04 14:58:59 +0000
26@@ -464,7 +464,7 @@
27 $scope.toggleFilter("hostname", "test", tab);
28 expect($scope.tabs[tab].filters).toEqual({
29 _: [],
30- hostname: ["test"]
31+ hostname: ["=test"]
32 });
33 });
34
35@@ -496,7 +496,7 @@
36 it("sets $scope.search", function() {
37 var controller = makeController();
38 $scope.toggleFilter("hostname", "test", tab);
39- expect($scope.tabs[tab].search).toBe("hostname:(test)");
40+ expect($scope.tabs[tab].search).toBe("hostname:(=test)");
41 });
42 });
43
44
45=== modified file 'src/maasserver/static/js/angular/filters/nodes.js'
46--- src/maasserver/static/js/angular/filters/nodes.js 2015-05-19 13:59:37 +0000
47+++ src/maasserver/static/js/angular/filters/nodes.js 2015-06-04 14:58:59 +0000
48@@ -45,15 +45,27 @@
49
50 // Return true when lowercase value contains the already
51 // lowercased lowerTerm.
52- function _matches(value, lowerTerm) {
53+ function _matches(value, lowerTerm, exact) {
54 if(angular.isNumber(value)) {
55- if(isInteger(value)) {
56- return value >= parseInt(lowerTerm, 10);
57+ if(exact) {
58+ if(isInteger(value)) {
59+ return value === parseInt(lowerTerm, 10);
60+ } else {
61+ return value === parseFloat(lowerTerm);
62+ }
63 } else {
64- return value >= parseFloat(lowerTerm);
65+ if(isInteger(value)) {
66+ return value >= parseInt(lowerTerm, 10);
67+ } else {
68+ return value >= parseFloat(lowerTerm);
69+ }
70 }
71 } else if(angular.isString(value)) {
72- return value.toLowerCase().indexOf(lowerTerm) >= 0;
73+ if(exact) {
74+ return value.toLowerCase() === lowerTerm;
75+ } else {
76+ return value.toLowerCase().indexOf(lowerTerm) >= 0;
77+ }
78 } else {
79 return value === lowerTerm;
80 }
81@@ -61,8 +73,8 @@
82
83 // Return true if value matches lowerTerm, unless negate is true then
84 // return false if matches.
85- function matches(value, lowerTerm, negate) {
86- var match = _matches(value, lowerTerm);
87+ function matches(value, lowerTerm, exact, negate) {
88+ var match = _matches(value, lowerTerm, exact);
89 if(negate) {
90 return !match;
91 }
92@@ -123,7 +135,7 @@
93 var i, j;
94 for(i = 0; i < terms.length; i++) {
95 var term = terms[i].toLowerCase();
96- var negate = false;
97+ var exact = false, negate = false;
98
99 // '!' at the beginning means the term is negated.
100 while(term.indexOf('!') === 0) {
101@@ -131,6 +143,18 @@
102 term = term.substring(1);
103 }
104
105+ // '=' at the beginning means to match exactly.
106+ if(term.indexOf('=') === 0) {
107+ exact = true;
108+ term = term.substring(1);
109+ }
110+
111+ // Allow '!' after the '=' as well.
112+ while(term.indexOf('!') === 0) {
113+ negate = !negate;
114+ term = term.substring(1);
115+ }
116+
117 if(angular.isArray(value)) {
118 // If value is an array check if the
119 // term matches any value in the
120@@ -141,7 +165,8 @@
121 // the array matches term.
122 var no_match = true;
123 for(j = 0; j < value.length; j++) {
124- if(matches(value[j], term, false)) {
125+ if(matches(
126+ value[j], term, exact, false)) {
127 no_match = false;
128 break; // Skip remaining tests.
129 }
130@@ -152,7 +177,8 @@
131 }
132 } else {
133 for(j = 0; j < value.length; j++) {
134- if(matches(value[j], term, false)) {
135+ if(matches(
136+ value[j], term, exact, false)) {
137 matched.push(node);
138 return;
139 }
140@@ -161,7 +187,7 @@
141 } else {
142 // Standard value check that it matches the
143 // term.
144- if(matches(value, term, negate)) {
145+ if(matches(value, term, exact, negate)) {
146 matched.push(node);
147 return;
148 }
149
150=== modified file 'src/maasserver/static/js/angular/filters/tests/test_nodes.js'
151--- src/maasserver/static/js/angular/filters/tests/test_nodes.js 2015-05-19 12:52:49 +0000
152+++ src/maasserver/static/js/angular/filters/tests/test_nodes.js 2015-06-04 14:58:59 +0000
153@@ -125,6 +125,17 @@
154 expect(nodesFilter(nodes, "hostname:!other")).toEqual([matchingNode]);
155 });
156
157+ it("matches on exact attribute", function() {
158+ var matchingNode = {
159+ hostname: "other"
160+ };
161+ var otherNode = {
162+ hostname: "other2"
163+ };
164+ var nodes = [matchingNode, otherNode];
165+ expect(nodesFilter(nodes, "hostname:=other")).toEqual([matchingNode]);
166+ });
167+
168 it("matches on array", function() {
169 var matchingNode = {
170 hostnames: ["name", "first"]
171@@ -305,6 +316,18 @@
172 [matchingNode]);
173 });
174
175+ it("matches an exact direct and a negated tag", function() {
176+ var matchingNode = {
177+ tags: ["first", "second"]
178+ };
179+ var otherNode = {
180+ tags: ["first1", "third"]
181+ };
182+ var nodes = [matchingNode, otherNode];
183+ expect(nodesFilter(nodes, "tags:(=first,!third)")).toEqual(
184+ [matchingNode]);
185+ });
186+
187 it("matches two negated tags", function() {
188 var matchingNode = {
189 tags: ["first", "second"]
190
191=== modified file 'src/maasserver/static/js/angular/services/search.js'
192--- src/maasserver/static/js/angular/services/search.js 2015-06-03 20:09:46 +0000
193+++ src/maasserver/static/js/angular/services/search.js 2015-06-04 14:58:59 +0000
194@@ -121,21 +121,42 @@
195 return search.trim();
196 };
197
198+ // Return the index of the value in the type for the filter.
199+ this._getFilterValueIndex = function(filters, type, value) {
200+ var values = filters[type];
201+ if(angular.isUndefined(values)) {
202+ return -1;
203+ }
204+ var lowerValues = values.map(function(value) {
205+ return value.toLowerCase();
206+ });
207+ return lowerValues.indexOf(value.toLowerCase());
208+ };
209+
210 // Return true if the type and value are in the filters.
211- this.isFilterActive = function(filters, type, value) {
212+ this.isFilterActive = function(filters, type, value, exact) {
213 var values = filters[type];
214 if(angular.isUndefined(values)) {
215 return false;
216 }
217- return values.indexOf(value) !== -1;
218+ if(angular.isUndefined(exact)) {
219+ exact = false;
220+ }
221+ if(exact) {
222+ value = "=" + value;
223+ }
224+ return this._getFilterValueIndex(filters, type, value) !== -1;
225 };
226
227 // Toggles a filter on or off based on type and value.
228- this.toggleFilter = function(filters, type, value) {
229+ this.toggleFilter = function(filters, type, value, exact) {
230 if(angular.isUndefined(filters[type])) {
231 filters[type] = [];
232 }
233- var idx = filters[type].indexOf(value);
234+ if(exact) {
235+ value = "=" + value;
236+ }
237+ var idx = this._getFilterValueIndex(filters, type, value);
238 if(idx === -1) {
239 filters[type].push(value);
240 } else {
241
242=== modified file 'src/maasserver/static/js/angular/services/tests/test_search.js'
243--- src/maasserver/static/js/angular/services/tests/test_search.js 2015-06-03 20:09:46 +0000
244+++ src/maasserver/static/js/angular/services/tests/test_search.js 2015-06-04 14:58:59 +0000
245@@ -114,6 +114,27 @@
246 type: ["valid"]
247 }, "type", "valid")).toBe(true);
248 });
249+
250+ it("returns false if exact value not in type", function() {
251+ expect(SearchService.isFilterActive(
252+ {
253+ type: ["valid"]
254+ }, "type", "valid", true)).toBe(false);
255+ });
256+
257+ it("returns true if exact value in type", function() {
258+ expect(SearchService.isFilterActive(
259+ {
260+ type: ["=valid"]
261+ }, "type", "valid", true)).toBe(true);
262+ });
263+
264+ it("returns true if lowercase value in type", function() {
265+ expect(SearchService.isFilterActive(
266+ {
267+ type: ["=Valid"]
268+ }, "type", "valid", true)).toBe(true);
269+ });
270 });
271
272 describe("toggleFilter", function() {
273@@ -144,6 +165,36 @@
274 type: ["exists"]
275 });
276 });
277+
278+ it("adds exact value to type in filters", function() {
279+ var filters = {
280+ type: ["exists"]
281+ };
282+ expect(SearchService.toggleFilter(
283+ filters, "type", "value", true)).toEqual({
284+ type: ["exists", "=value"]
285+ });
286+ });
287+
288+ it("removes exact value to type in filters", function() {
289+ var filters = {
290+ type: ["exists", "value", "=value"]
291+ };
292+ expect(SearchService.toggleFilter(
293+ filters, "type", "value", true)).toEqual({
294+ type: ["exists", "value"]
295+ });
296+ });
297+
298+ it("removes lowercase value to type in filters", function() {
299+ var filters = {
300+ type: ["exists", "=Value"]
301+ };
302+ expect(SearchService.toggleFilter(
303+ filters, "type", "value", true)).toEqual({
304+ type: ["exists"]
305+ });
306+ });
307 });
308
309 describe("getEmptyFilter", function() {