Skip to content

Commit f41117a

Browse files
authored
Merge pull request #161 from querqy/usage_heat_bars
Usage heat bars (v4.3.0)
2 parents 80b524e + cc64081 commit f41117a

File tree

8 files changed

+89
-20
lines changed

8 files changed

+89
-20
lines changed

app/controllers/ApiController.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ class ApiController @Inject()(val controllerComponents: SecurityComponents,
250250
def listAll(solrIndexId: String) : Action[AnyContent] = Action {
251251
val searchInputs = searchManagementRepository.listAllSearchInputsInclDirectedSynonyms(SolrIndexId(solrIndexId))
252252
val spellings = searchManagementRepository.listAllSpellingsWithAlternatives(SolrIndexId(solrIndexId))
253-
Ok(Json.toJson(ListItem.create(searchInputs, spellings)))
253+
Ok(Json.toJson(ListItem.create(searchInputs, spellings, rulesUsageService.getRulesUsageStatistics)))
254254
}
255255

256256
def addNewSpelling(solrIndexId: String): Action[AnyContent] = Action.async { request: Request[AnyContent] =>

app/models/input/ListItem.scala

+22-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package models.input
33
import models.input.ListItemType.ListItemType
44
import models.spellings.{CanonicalSpelling, CanonicalSpellingWithAlternatives}
55
import play.api.libs.json.{Format, Json, OFormat}
6+
import services.RulesUsage
67

78
object ListItemType extends Enumeration {
89
type ListItemType = Value
@@ -18,12 +19,30 @@ case class ListItem(id: String,
1819
synonyms: Seq[String] = Seq.empty,
1920
tags: Seq[InputTag] = Seq.empty,
2021
comment: String = "",
21-
additionalTermsForSearch: Seq[String] = Seq.empty)
22+
additionalTermsForSearch: Seq[String] = Seq.empty,
23+
usageFrequency: Option[Int] = None)
2224

2325
object ListItem {
24-
def create(searchInputs: Seq[SearchInputWithRules], spellings: Seq[CanonicalSpellingWithAlternatives]): Seq[ListItem] = {
26+
27+
def create(searchInputs: Seq[SearchInputWithRules],
28+
spellings: Seq[CanonicalSpellingWithAlternatives],
29+
optRuleUsageStatistics: Option[Seq[RulesUsage]]): Seq[ListItem] = {
2530
val listItems = listItemsForRules(searchInputs) ++ listItemsForSpellings(spellings)
26-
listItems.sortBy(_.term.trim.toLowerCase.replace("\"", ""))
31+
// augment with usage statistics, only if available, pass through otherwise
32+
val listItemsWithUsageStatistics = optRuleUsageStatistics match {
33+
case Some(rulesUsage) if rulesUsage.isEmpty => listItems
34+
case Some(ruleUsage) => augmentRulesWithUsage(listItems, ruleUsage)
35+
case None => listItems
36+
}
37+
listItemsWithUsageStatistics.sortBy(_.term.trim.toLowerCase.replace("\"", ""))
38+
}
39+
40+
private def augmentRulesWithUsage(listItems: Seq[ListItem], ruleUsage: Seq[RulesUsage]): Seq[ListItem] = {
41+
// there can be multiple rule usage items for the the same rule, one per keyword combination that triggered the usage
42+
val combinedRuleUsageFrequency = ruleUsage.groupBy(_.inputId.id).view.mapValues(_.map(_.frequency).sum)
43+
listItems.map { listItem =>
44+
listItem.copy(usageFrequency = combinedRuleUsageFrequency.get(listItem.id))
45+
}
2746
}
2847

2948
private def listItemsForRules(searchInputs: Seq[SearchInputWithRules]): Seq[ListItem] = {

build.sbt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import com.typesafe.sbt.GitBranchPrompt
22

33
name := "search-management-ui"
4-
version := "4.2.0"
4+
version := "4.3.0"
55
maintainer := "Contact productful.io <hello@productful.io>"
66

77
scalaVersion := "2.13.14"

frontend/src/app/components/rules-panel/rules-list/rules-list.component.css

+14
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,17 @@ li.list-group-item.active > .text-muted {
5353
.smui-align-middle {
5454
vertical-align: middle;
5555
}
56+
57+
.smui-frequency-box-container {
58+
display: inline-block;
59+
width: 32px;
60+
height: 28px;
61+
border-left: 1px solid #666;
62+
}
63+
64+
.smui-frequency-box {
65+
display: inline-block;
66+
height: 1rem;
67+
background-image: linear-gradient(to right, #db5f57, #dbc257, #91db57, #57db80, #57d3db, #5770db, #a157db, #db57b2);
68+
background-size: 35px 16px;
69+
}

frontend/src/app/components/rules-panel/rules-list/rules-list.component.html

+20-12
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,26 @@
2727
}"
2828
(click)="selectListItemWithCheck(listItem)"
2929
>
30+
<div class="smui-frequency-box-container smui-right-gap" *ngIf="shouldDisplayUsageFrequency()">
31+
<span class="smui-frequency-box align-middle"
32+
title="Rule has been applied {{listItem.usageFrequency || '0'}} time(s)."
33+
[style.width.px]="4 * (listItem.usageFrequencyBucket || 0) + 2"
34+
>
35+
</span>
36+
</div>
37+
3038
<span class="smui-right-gap smui-align-middle">
31-
<i
32-
*ngIf="listItem.itemType.toString() === 'RuleManagement'"
33-
class="fa fa-list smui-align-middle"
34-
aria-hidden="true"
35-
></i>
36-
<i
37-
*ngIf="listItem.itemType.toString() === 'Spelling'"
38-
class="fa fa-book smui-align-middle"
39-
aria-hidden="true"
40-
></i>
41-
</span>
39+
<i
40+
*ngIf="listItem.itemType.toString() === 'RuleManagement'"
41+
class="fa fa-list smui-align-middle"
42+
aria-hidden="true"
43+
></i>
44+
<i
45+
*ngIf="listItem.itemType.toString() === 'Spelling'"
46+
class="fa fa-book smui-align-middle"
47+
aria-hidden="true"
48+
></i>
49+
</span>
4250

4351
<span class="smui-align-middle list-item-term">{{ listItem.term }}</span>
4452

@@ -68,7 +76,7 @@
6876

6977
<div ngbDropdownMenu aria-labelledby="actionsDropdown">
7078
<button ngbDropdownItem
71-
[disabled]="listItem.itemType.toString() !== 'RuleManagement'"
79+
[disabled]="listItem.itemType.toString() !== 'Rule*Management'"
7280
(click)="copyRuleItem(listItem.id, $event)"
7381
>
7482
<i class="fa fa-clone mr-3"></i>Copy to

frontend/src/app/components/rules-panel/rules-list/rules-list.component.ts

+4
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ export class RulesListComponent implements OnChanges {
134134
this.isShowingAllItems = !this.isShowingAllItems;
135135
}
136136

137+
shouldDisplayUsageFrequency() {
138+
return this.featureToggleService.getSyncToggleRuleUsageStatistics();
139+
}
140+
137141
private selectListItem(listItem?: ListItem) {
138142
console.log(
139143
`In SearchInputListComponent :: selectListItem :: id = ${

frontend/src/app/models/list.model.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ export class ListItem {
1414
tags: Array<InputTag>;
1515
comment: string;
1616
additionalTermsForSearch: Array<string>;
17+
usageFrequency: number | null;
18+
usageFrequencyBucket: number | undefined;
1719
}

frontend/src/app/services/list-items.service.ts

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable } from '@angular/core';
22
import { HttpClient } from '@angular/common/http';
3-
3+
import { map } from 'rxjs/operators';
44
import { ListItem } from '../models';
55

66
@Injectable({
@@ -14,7 +14,29 @@ export class ListItemsService {
1414

1515
getAllItemsForInputList(solrIndexId: string): Promise<Array<ListItem>> {
1616
return this.http
17-
.get<ListItem[]>(`${this.baseUrl}/${solrIndexId}/${this.listItemsPath}`)
18-
.toPromise();
17+
.get(`${this.baseUrl}/${solrIndexId}/${this.listItemsPath}`)
18+
.pipe(
19+
map((response: any) =>
20+
(response as []).map((item) => Object.assign(new ListItem, item))),
21+
map((listItems: ListItem[]) =>
22+
this.assignRuleUsageBuckets(listItems, 8)
23+
)
24+
).toPromise();
25+
}
26+
27+
assignRuleUsageBuckets(listItems: ListItem[], numBuckets: number): ListItem[] {
28+
if (listItems.length == 0) {
29+
return listItems;
30+
}
31+
// perform a min-max normalization of the log2 of the usage frequency and assign them to $numBuckets buckets
32+
const log2Values = listItems.map(listItem => Math.log2(listItem.usageFrequency || 1));
33+
const minLog2 = Math.min(...log2Values);
34+
const maxLog2 = Math.max(...log2Values);
35+
const originalRange = maxLog2 - minLog2;
36+
return listItems.map(listItem => {
37+
const log2Value = Math.log2(listItem.usageFrequency || 1);
38+
const bucket = Math.round(((log2Value - minLog2) / originalRange) * numBuckets);
39+
return Object.assign(listItem, {"usageFrequencyBucket": bucket})
40+
});
1941
}
2042
}

0 commit comments

Comments
 (0)