|
24 | 24 | {{ showHidden ? "Hide Triaged" : "Show Triaged" }} |
25 | 25 | </button> |
26 | 26 | </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> |
34 | 59 | </div> |
35 | 60 | <div v-else> |
36 | 61 | <div v-if="canEdit" class="btn-group" role="group"> |
|
48 | 73 | { name: 'bug__external_type__classname', type: 'String' }, |
49 | 74 | { name: 'bug__external_type__hostname', type: 'String' }, |
50 | 75 | { name: 'description', type: 'String' }, |
| 76 | + { name: 'domain', type: 'String' }, |
| 77 | + { name: 'domain__endswith', type: 'String' }, |
| 78 | + { name: 'domain__isnull', type: 'Boolean' }, |
51 | 79 | { name: 'priority', type: 'Integer' }, |
52 | 80 | { name: 'signature', type: 'String' }, |
53 | 81 | { name: 'size', type: 'Integer' }, |
@@ -193,6 +221,7 @@ import _isEqual from "lodash/isEqual"; |
193 | 221 | import ClipLoader from "vue-spinner/src/ClipLoader.vue"; |
194 | 222 | import { errorParser, multiSort, parseHash } from "../../helpers"; |
195 | 223 | import * as api from "../../api"; |
| 224 | +import { MatchObjects } from "../../helpers"; |
196 | 225 | import PageNav from "../PageNav.vue"; |
197 | 226 | import Row from "./Row.vue"; |
198 | 227 | import HelpJSONQueryPopover from "../HelpJSONQueryPopover.vue"; |
@@ -237,13 +266,27 @@ export default { |
237 | 266 | "size", |
238 | 267 | ]; |
239 | 268 | 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 | + }; |
240 | 281 |
|
241 | 282 | return { |
242 | 283 | advancedQuery: false, |
243 | 284 | buckets: [], |
244 | 285 | currentEntries: "?", |
245 | 286 | currentPage: 1, |
246 | 287 | defaultSortKeys: defaultSortKeys, |
| 288 | + domainFilter: "", |
| 289 | + domainFilterSignature: domainFilterSignature, |
247 | 290 | loading: false, |
248 | 291 | modifiedCache: {}, |
249 | 292 | pageSize: 100, |
@@ -280,7 +323,15 @@ export default { |
280 | 323 | } |
281 | 324 | } |
282 | 325 | 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 | + } |
284 | 335 | } |
285 | 336 | } |
286 | 337 | this.fetch(); |
@@ -347,6 +398,36 @@ export default { |
347 | 398 | } |
348 | 399 | this.fetch(); |
349 | 400 | }, |
| 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 | + }, |
350 | 431 | buildParams() { |
351 | 432 | return { |
352 | 433 | vue: "1", |
@@ -438,6 +519,16 @@ export default { |
438 | 519 | </script> |
439 | 520 |
|
440 | 521 | <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 | +} |
441 | 532 | .m-strong { |
442 | 533 | margin-top: 1.5rem; |
443 | 534 | margin-bottom: 1.5rem; |
|
0 commit comments