Skip to content

Commit 600078f

Browse files
authored
Merge pull request #57 from MozillaSecurity/domain_filter
Add a filter for domains
2 parents 1d460b6 + 3dc9aa7 commit 600078f

3 files changed

Lines changed: 462 additions & 8 deletions

File tree

server/frontend/src/components/Buckets/List.vue

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,38 @@
2424
{{ showHidden ? "Hide Triaged" : "Show Triaged" }}
2525
</button>
2626
</div>
27-
<p></p>
28-
<a
29-
title="Show advanced query for the current search/filters"
30-
class="pointer"
31-
v-on:click="showAdvancedQuery"
32-
>Advanced query</a
33-
><br />
27+
<details>
28+
<summary>Filters</summary>
29+
<div class="input-group">
30+
<label for="domain-filter">Domain:</label>
31+
<input
32+
id="domain-filter"
33+
type="text"
34+
placeholder="e.g., example.com (includes subdomains)"
35+
v-model="domainFilter"
36+
v-on:keyup.enter="updateDomainFilter"
37+
:disabled="loading"
38+
style="max-width: 400px"
39+
/>
40+
</div>
41+
<div class="input-group">
42+
<button
43+
type="button"
44+
class="btn btn-secondary"
45+
:disabled="loading"
46+
title="Apply filters"
47+
v-on:click="updateDomainFilter"
48+
>
49+
Apply
50+
</button>
51+
</div>
52+
<a
53+
title="Show advanced query for the current search/filters"
54+
class="pointer"
55+
v-on:click="showAdvancedQuery"
56+
>Advanced query</a
57+
>
58+
</details>
3459
</div>
3560
<div v-else>
3661
<div v-if="canEdit" class="btn-group" role="group">
@@ -48,6 +73,9 @@
4873
{ name: 'bug__external_type__classname', type: 'String' },
4974
{ name: 'bug__external_type__hostname', type: 'String' },
5075
{ name: 'description', type: 'String' },
76+
{ name: 'domain', type: 'String' },
77+
{ name: 'domain__endswith', type: 'String' },
78+
{ name: 'domain__isnull', type: 'Boolean' },
5179
{ name: 'priority', type: 'Integer' },
5280
{ name: 'signature', type: 'String' },
5381
{ name: 'size', type: 'Integer' },
@@ -193,6 +221,7 @@ import _isEqual from "lodash/isEqual";
193221
import ClipLoader from "vue-spinner/src/ClipLoader.vue";
194222
import { errorParser, multiSort, parseHash } from "../../helpers";
195223
import * as api from "../../api";
224+
import { MatchObjects } from "../../helpers";
196225
import PageNav from "../PageNav.vue";
197226
import Row from "./Row.vue";
198227
import HelpJSONQueryPopover from "../HelpJSONQueryPopover.vue";
@@ -237,13 +266,27 @@ export default {
237266
"size",
238267
];
239268
const defaultSortKeys = ["-size", "-latest_report"];
269+
const domainFilterSignature = {
270+
op: "AND",
271+
1: {
272+
op: "AND",
273+
0: {
274+
op: "OR",
275+
domain: MatchObjects.ANY,
276+
domain__endswith: MatchObjects.ANY,
277+
},
278+
domain__isnull: MatchObjects.ANY,
279+
},
280+
};
240281
241282
return {
242283
advancedQuery: false,
243284
buckets: [],
244285
currentEntries: "?",
245286
currentPage: 1,
246287
defaultSortKeys: defaultSortKeys,
288+
domainFilter: "",
289+
domainFilterSignature: domainFilterSignature,
247290
loading: false,
248291
modifiedCache: {},
249292
pageSize: 100,
@@ -280,7 +323,15 @@ export default {
280323
}
281324
}
282325
if (Object.prototype.hasOwnProperty.call(hash, "query")) {
283-
this.queryStr = JSON.stringify(JSON.parse(hash.query || ""), null, 2);
326+
const parsedQuery = JSON.parse(hash.query || "");
327+
this.queryStr = JSON.stringify(parsedQuery, null, 2);
328+
// Extract domain filter if present
329+
const matcher = new MatchObjects();
330+
if (matcher.match(parsedQuery, this.domainFilterSignature)) {
331+
this.domainFilter = parsedQuery[1][0].domain;
332+
} else if (parsedQuery.domain) {
333+
this.domainFilter = parsedQuery.domain;
334+
}
284335
}
285336
}
286337
this.fetch();
@@ -347,6 +398,36 @@ export default {
347398
}
348399
this.fetch();
349400
},
401+
updateDomainFilter() {
402+
const domainFilter = this.domainFilter.trim();
403+
let query = JSON.parse(this.queryStr);
404+
405+
const matcher = new MatchObjects();
406+
if (matcher.match(query, this.domainFilterSignature)) {
407+
query = query[0];
408+
}
409+
410+
if (domainFilter) {
411+
const domainQuery = {
412+
op: "AND",
413+
0: {
414+
op: "OR",
415+
domain: domainFilter,
416+
domain__endswith: "." + domainFilter,
417+
},
418+
domain__isnull: false,
419+
};
420+
421+
query = {
422+
op: "AND",
423+
0: query,
424+
1: domainQuery,
425+
};
426+
}
427+
this.queryStr = JSON.stringify(query, null, 2);
428+
429+
this.fetch();
430+
},
350431
buildParams() {
351432
return {
352433
vue: "1",
@@ -438,6 +519,16 @@ export default {
438519
</script>
439520

440521
<style scoped>
522+
details {
523+
margin-top: 1.5rem;
524+
}
525+
details::details-content {
526+
margin-left: 1.5rem;
527+
}
528+
summary {
529+
font-weight: bold;
530+
display: list-item;
531+
}
441532
.m-strong {
442533
margin-top: 1.5rem;
443534
margin-bottom: 1.5rem;

server/frontend/src/helpers.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,35 @@ export const multiSort = {
191191
},
192192
},
193193
};
194+
195+
export class MatchObjects {
196+
static ANY = {};
197+
198+
match(obj, signature) {
199+
const objKeys = Object.keys(obj);
200+
const sigKeys = Object.keys(signature);
201+
// Check if signature keys are a subset of obj keys
202+
for (const key of sigKeys) {
203+
if (!objKeys.includes(key)) {
204+
return false;
205+
}
206+
}
207+
for (const [key, sigValue] of Object.entries(signature)) {
208+
let value = obj[key];
209+
if (sigValue === MatchObjects.ANY) {
210+
continue;
211+
}
212+
if (typeof value != typeof sigValue) {
213+
return false;
214+
}
215+
if (typeof value == "object" && value !== null) {
216+
if (!this.match(value, sigValue)) {
217+
return false;
218+
}
219+
} else if (value !== sigValue) {
220+
return false;
221+
}
222+
}
223+
return true;
224+
}
225+
}

0 commit comments

Comments
 (0)