Skip to content

Commit ccb0405

Browse files
authored
Extractor Catalog: search filters + matching ANY/ALL (#120)
* Sync to test another PR * First pass at generic search filter + matching options, stubbed out space/metadata stuff from @max-zilla's branch * Stability/sanity changes from testing - single form, no enter key submission, explicit "Submit" button * Preliminary work done for searching via metadata/space * fix typo * Metadata/space search appears to work, matching seems to return sensible values * Fix edge case for generic search + other filters * Properly handle base case of matching=any with no filters
1 parent 50aa52e commit ccb0405

File tree

2 files changed

+205
-54
lines changed

2 files changed

+205
-54
lines changed

app/controllers/Extractors.scala

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package controllers
22

3-
import models.{ExtractorInfo, Folder, ResourceRef, UUID, Extraction}
3+
import models.{Extraction, ExtractorInfo, Folder, ResourceRef, UUID}
44
import play.api.mvc.Controller
55
import api.Permission
66
import javax.inject.{Inject, Singleton}
@@ -14,6 +14,9 @@ import java.text.SimpleDateFormat
1414
import java.util.concurrent.TimeUnit
1515
import java.util.Calendar
1616

17+
import api.Permission.Permission
18+
import play.api.libs.json.Json
19+
1720
/**
1821
* Information about extractors.
1922
*/
@@ -53,28 +56,111 @@ class Extractors @Inject() (extractions: ExtractionService,
5356
*/
5457
def selectExtractors() = AuthenticatedAction { implicit request =>
5558
implicit val user = request.user
56-
// Filter extractors by the trigger type if necessary
57-
var runningExtractors: List[ExtractorInfo] = extractorService.listExtractorsInfo(List.empty)
5859

59-
request.getQueryString("processTriggerSearchFilter") match {
60-
case Some("file/*") => runningExtractors = runningExtractors.filter(re => re.process.file.length > 0)
61-
case Some("dataset/*") => runningExtractors = runningExtractors.filter(re => re.process.dataset.length > 0)
62-
case Some("metadata/*") => runningExtractors = runningExtractors.filter(re => re.process.metadata.length > 0)
63-
case None => {}
64-
}
60+
// Filter extractors by user filters necessary
61+
var runningExtractors: List[ExtractorInfo] = extractorService.listExtractorsInfo(List.empty)
6562
val selectedExtractors: List[String] = extractorService.getEnabledExtractors()
6663
val groups = extractions.groupByType(extractions.findAll())
6764
val allLabels = extractorService.listExtractorsLabels()
6865
val categorizedLabels = allLabels.groupBy(_.category.getOrElse("Other"))
69-
request.getQueryString("labelFilter") match {
70-
case None => {}
71-
case Some(lblName) => allLabels.find(lbl => lblName == lbl.name) match {
72-
case None => {}
73-
case Some(label) => runningExtractors = runningExtractors.filter(re => label.extractors.contains(re.name))
66+
val allSpaces = spaces.listAccess(0, Set[Permission](Permission.ViewSpace), request.user, false, false, false, false)
67+
68+
// Fetch metadata contexts from ExtractorInfo
69+
var allMetadata = Map[String, String]()
70+
runningExtractors.foreach(re => {
71+
val currentContexts: List[Map[String, String]] = re.contexts.as[List[Map[String,String]]]
72+
currentContexts.foreach((contextMap) => {
73+
contextMap.foreach(entry => {
74+
val key = entry._1
75+
val value = entry._2
76+
allMetadata = allMetadata ++ Map[String, String](key -> value)
77+
})
78+
})
79+
})
80+
81+
val labelSearch = request.getQueryString("labelFilter").getOrElse("")
82+
val genericSearch = request.getQueryString("genericSearchFilter").getOrElse("")
83+
val metadataSearch = request.getQueryString("metadataSearchFilter").getOrElse("")
84+
val spaceSearch = request.getQueryString("spaceSearchFilter").getOrElse("")
85+
val processTriggerSearch = request.getQueryString("processTriggerSearchFilter").getOrElse("")
86+
87+
// Short-circuit for no filters => return the full list
88+
if (labelSearch.isEmpty && genericSearch.isEmpty && metadataSearch.isEmpty && spaceSearch.isEmpty && processTriggerSearch.isEmpty) {
89+
Ok(views.html.updateExtractors(runningExtractors, selectedExtractors, groups, categorizedLabels, allSpaces, allMetadata))
90+
} else {
91+
// Default value for unmatched filter - important for ANY vs ALL
92+
// Returning the full list by default in part of an ANY (OR) query will always return the full list
93+
val defaultValue: List[ExtractorInfo] = request.getQueryString("matching") match {
94+
case Some("any") => List[ExtractorInfo]()
95+
case _ => runningExtractors
96+
}
97+
98+
// TODO: Autocomplete on text field from dynamically generated list of distinct triggers
99+
val triggerMatches: List[ExtractorInfo] = request.getQueryString("processTriggerSearchFilter") match {
100+
case Some("file/*") => runningExtractors.filter(re => re.process.file.length > 0)
101+
case Some("dataset/*") => runningExtractors.filter(re => re.process.dataset.length > 0)
102+
case Some("metadata/*") => runningExtractors.filter(re => re.process.metadata.length > 0)
103+
/*case Some(filt) if (filt.length > 0) => {
104+
// TODO: Need to figure out how to effectively search the ProcessTriggers structure
105+
}*/
106+
case _ => defaultValue
74107
}
75-
}
76108

77-
Ok(views.html.updateExtractors(runningExtractors, selectedExtractors, groups, categorizedLabels))
109+
// Stringify full resource to perform simple search for user's query
110+
val genericMatches: List[ExtractorInfo] = request.getQueryString("genericSearchFilter") match {
111+
case Some(query) => runningExtractors.filter(re => Json.toJson(re).toString.contains(query))
112+
case _ => defaultValue
113+
}
114+
115+
// For the chosen space id, remove any extractor that is not enabled extractors for that space
116+
val spaceMatches: List[ExtractorInfo] = request.getQueryString("spaceSearchFilter") match {
117+
case Some(spaceid) if spaceid.length > 0 => {
118+
// TODO: Wire it up so users see SpaceName but we pass ID... append (ID) to duplicate space names
119+
spaces.getAllExtractors(UUID(spaceid)) match {
120+
case Some(spaceExtractors) => runningExtractors.filter(re => spaceExtractors.enabled.contains(re.name))
121+
case _ => List[ExtractorInfo]()
122+
}
123+
}
124+
case _ => defaultValue
125+
}
126+
127+
// Match metadata contexts from ExtractorInfo
128+
val metadataMatches: List[ExtractorInfo] = request.getQueryString("metadataSearchFilter") match {
129+
case Some(metafield) if metafield.length > 0 => {
130+
runningExtractors.filter(re => {
131+
var ret = false
132+
val extractorContexts = re.contexts.as[List[Map[String,String]]]
133+
extractorContexts.takeWhile(_ => !ret).foreach((context) => {
134+
context.takeWhile(_ => !ret).foreach(entry => {
135+
val key = entry._1
136+
val value = entry._2
137+
if (key == metafield) {
138+
ret = true
139+
}
140+
})
141+
})
142+
ret
143+
})
144+
}
145+
case _ => defaultValue
146+
}
147+
148+
// Filter based on selected label
149+
val labelMatches: List[ExtractorInfo] = request.getQueryString("labelFilter") match {
150+
case Some(lblName) => allLabels.find(lbl => lblName == lbl.name) match {
151+
case Some(label) => runningExtractors.filter(re => label.extractors.contains(re.name))
152+
case _ => defaultValue
153+
}
154+
case _ => defaultValue
155+
}
156+
157+
val matches: List[ExtractorInfo] = request.getQueryString("matching") match {
158+
case Some("any") => triggerMatches.toSet.union(genericMatches.toSet).union(spaceMatches.toSet).union(metadataMatches.toSet).union(labelMatches.toSet).toList
159+
case _ => triggerMatches.toSet.intersect(genericMatches.toSet).intersect(spaceMatches.toSet).intersect(metadataMatches.toSet).intersect(labelMatches.toSet).toList
160+
}
161+
162+
Ok(views.html.updateExtractors(matches, selectedExtractors, groups, categorizedLabels, allSpaces, allMetadata))
163+
}
78164
}
79165

80166
def manageLabels = ServerAdminAction { implicit request =>

app/views/updateExtractors.scala.html

Lines changed: 103 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@(runningExtractors: List[ExtractorInfo], selectedExtractors: List[String], groups: Map[String, ExtractionGroup], labelCategories: Map[String, List[ExtractorsLabel]],
1+
@(runningExtractors: List[ExtractorInfo], selectedExtractors: List[String], groups: Map[String, ExtractionGroup], labelCategories: Map[String, List[ExtractorsLabel]], allSpaces: List[ProjectSpace], allMetadata: Map[String, String],
22
showOptional: Map[String,Boolean] = Map("additionalInfo"->false, "processTriggers"->false, "labelSection"->true, "filterSection"->true, "ratings"->false))(implicit user: Option[models.User])
33
@import _root_.util.Formatters._
44
@import helper._
@@ -23,8 +23,8 @@ <h1>Extractor Catalog</h1>
2323
</div>
2424
}
2525

26-
<form class="form-horizontal">
27-
<!-- TODO: Label search filters -->
26+
<div class="form-horizontal" id="searchFiltersForm">
27+
<!-- Label search filter -->
2828
@if(showOptional("labelSection")) {
2929
<fieldset class="labels-border">
3030
<legend class="labels-border">Labels</legend>
@@ -44,8 +44,8 @@ <h1>Extractor Catalog</h1>
4444
<div class="control-group">
4545
<div class="btn-group" data-toggle="buttons">
4646
@for(label <- catLabels) {
47-
<button onclick="submitFilters('@label.name', this)" id="[email protected]" role="button" class="btn btn-primary label-filter-btn">
48-
<input type="checkbox" autocomplete="off" checked> @label.name
47+
<button type="button" onclick="submitFilters('@label.name')" id="[email protected]" name="[email protected]" role="button" class="btn btn-primary label-filter-btn">
48+
<input type="checkbox" autocomplete="off" name="[email protected]" checked> @label.name
4949
</button>
5050
}
5151
</div>
@@ -64,18 +64,19 @@ <h1>Extractor Catalog</h1>
6464
<div class="row">
6565
<div class="col-sm-5">
6666

67-
<!-- TODO: Generic search filter -->
67+
<!-- Generic search filter -->
6868
<div class="form-group">
6969
<div class="col-sm-12">
70-
<input type="text" class="form-control" id="genericSearchFilter" placeholder="Search for extractors...">
70+
<input type="text" class="form-control" id="genericSearchFilter" name="genericSearchFilter" placeholder="Search for extractor name, contributors, etc...">
7171
</div>
7272
</div>
7373

7474
<!-- Process trigger search filter -->
7575
<div class="form-group">
7676
<label for="processTriggerSearchFilter" class="col-sm-3 control-label">Triggers On:</label>
7777
<div class="col-sm-9">
78-
<select class="form-control" name="processTriggerSearchFilter" id="processTriggerSearchFilter" onchange="submitFilters(null, this)">
78+
<select class="form-control" name="processTriggerSearchFilter" id="processTriggerSearchFilter">
79+
<option></option>
7980
<option>file/*</option>
8081
<option>dataset/*</option>
8182
<option>metadata/*</option>
@@ -91,9 +92,10 @@ <h1>Extractor Catalog</h1>
9192
<label for="spaceSearchFilter" class="col-sm-4 control-label">Within Space:</label>
9293
<div class="col-sm-8">
9394
<select class="form-control" name="spaceSearchFilter" id="spaceSearchFilter">
94-
<option>Space A</option>
95-
<option>Space B</option>
96-
<option>Space C</option>
95+
<option></option>
96+
@for(space <- allSpaces) {
97+
<option value="@space.id">@space.name</option>
98+
}
9799
</select>
98100
</div>
99101
</div>
@@ -103,34 +105,42 @@ <h1>Extractor Catalog</h1>
103105
<label for="metadataSearchFilter" class="col-sm-4 control-label">Produces Metadata:</label>
104106
<div class="col-sm-8">
105107
<select class="form-control" name="metadataSearchFilter" id="metadataSearchFilter">
106-
<option>Key 1</option>
107-
<option>Key 2</option>
108-
<option>Key 3</option>
108+
<option></option>
109+
@for((key, value) <- allMetadata) {
110+
<option>@key</option>
111+
}
109112
</select>
110113
</div>
111114
</div>
112115
</div>
113116

114-
<!-- TODO: Match ALL/ANY filter radio buttons -->
117+
<!-- Match ALL/ANY filter radio buttons -->
115118
<div class="col-sm-2">
116-
<div class="radio">
117-
<label>
118-
<input type="radio" name="matching" id="matchAny" autocomplete="off" checked> Match ANY filter
119-
</label>
119+
<div class="row">
120+
<div class="col-sm-12">
121+
<div class="radio">
122+
<label>
123+
<input type="radio" name="matching" id="matchAny" autocomplete="off" checked value="any"> Match ANY filter
124+
</label>
125+
</div>
126+
<div class="radio">
127+
<label>
128+
<input type="radio" name="matching" id="matchAll" autocomplete="off" value="all"> Match ALL filters
129+
</label>
130+
</div>
131+
</div>
120132
</div>
121-
<div class="radio">
122-
<label>
123-
<input type="radio" name="matching" id="matchAll" autocomplete="off"> Match ALL filters
124-
</label>
133+
<div class="row" style="margin-top:12px">
134+
<div class="col-sm-12">
135+
<button type="submit" class="btn btn-sm btn-primary" onclick="submitFilters(null)"><span class="glyphicon glyphicon-search"></span> Search</button>
136+
</div>
125137
</div>
126138
</div>
127139
</div>
128-
129-
130140
</div>
131141
</fieldset>
132142
}
133-
</form>
143+
</div>
134144

135145
<div class="row top-padding">
136146
<div class="col-xs-12">
@@ -345,29 +355,84 @@ <h1>Extractor Catalog</h1>
345355
</div>
346356

347357
<script>
348-
function submitFilters(labelName, context) {
358+
function submitFilters(labelName) {
349359
// Grab existing query string values
350-
var currentParams = new URLSearchParams(window.location.search);
351-
var targets = context.formAction.split('@routes.Extractors.selectExtractors()');
360+
const currentParams = new URLSearchParams(window.location.search);
352361

353362
// If same filter selected twice, de-select it
354-
var currentLabel = currentParams.get('labelFilter');
355-
var newLabelName = labelName === currentLabel ? null : (labelName || currentLabel);
363+
const currentLabel = currentParams.get('labelFilter');
364+
const newLabelName = labelName === currentLabel ? null : (labelName || currentLabel);
365+
const newGenericSearch = $('#genericSearchFilter').val();
366+
const newProcessTrigger = $('#processTriggerSearchFilter').val();
367+
const newSpace = $('#spaceSearchFilter').val();
368+
const newMetadata = $('#metadataSearchFilter').val();
369+
370+
// Set new label query strings parameter based on new filters
371+
const newParams = new URLSearchParams();
372+
if (newLabelName && newLabelName != "") {
373+
newParams.set('labelFilter', newLabelName);
374+
} else {
375+
newParams.delete('labelFilter');
376+
}
377+
378+
if (newGenericSearch && newGenericSearch != "") {
379+
newParams.set('genericSearchFilter', newGenericSearch);
380+
} else {
381+
newParams.delete('genericSearchFilter');
382+
}
356383

357-
// Set new query strings parameters based on form value
358-
var newParams = new URLSearchParams(targets[1]);
359-
newLabelName ? newParams.set('labelFilter', newLabelName) : newParams.delete('labelFilter');
384+
if (newProcessTrigger && newProcessTrigger != "") {
385+
newParams.set('processTriggerSearchFilter', newProcessTrigger);
386+
} else {
387+
newParams.delete('processTriggerSearchFilter');
388+
}
389+
390+
if (newSpace && newSpace != "") {
391+
newParams.set('spaceSearchFilter', newSpace);
392+
} else {
393+
newParams.delete('spaceSearchFilter');
394+
}
395+
396+
if (newMetadata && newMetadata != "") {
397+
newParams.set('metadataSearchFilter', newMetadata);
398+
} else {
399+
newParams.delete('metadataSearchFilter');
400+
}
401+
402+
// Set filter matching ANY / ALL
403+
if ($('#matchAll').prop('checked')) {
404+
newParams.set('matching', 'all');
405+
} else if ($('#matchAny').prop('checked')) {
406+
newParams.set('matching', 'any');
407+
} else {
408+
// This should never be possible - one radio should always be checked
409+
newParams.delete('matching');
410+
}
360411

412+
// Set all other query strings parameters based on form values
361413
window.location.search = newParams.toString();
362414
}
363415

364416
$('document').ready(function () {
365417
var params = new URLSearchParams(window.location.search);
366-
var processTriggerParam = "processTriggerSearchFilter"
367-
if (params.has(processTriggerParam)) {
368-
$(processTriggerParam).val(params.get(processTriggerParam));
369-
}
370418
var labelParam = "labelFilter"
419+
var targetParameters = ["genericSearchFilter", "processTriggerSearchFilter", "spaceSearchFilter","metadataSearchFilter"];
420+
for (let idx in targetParameters) {
421+
var targetParameter = targetParameters[idx];
422+
if (params.has(targetParameter)) {
423+
document.getElementById(targetParameter).value = params.get(targetParameter);
424+
}
425+
}
426+
427+
if (params.has("matching")) {
428+
const matching = params.get("matching");
429+
if (matching === "all") {
430+
$('#matchAll').prop('checked', true).trigger("click");
431+
} else if (matching === "any") {
432+
$('#matchAny').prop('checked', true).trigger("click");
433+
}
434+
}
435+
371436
if (params.has(labelParam)) {
372437
// If we have a label param, uncheck all other labels
373438
$('.label-filter-btn').removeClass("btn-primary");

0 commit comments

Comments
 (0)