Skip to content

Commit 74c72ce

Browse files
authored
Include tag counts in tag filter & tag input autocomplete (#2711)
1 parent 80a225c commit 74c72ce

File tree

10 files changed

+153
-72
lines changed

10 files changed

+153
-72
lines changed

backend/btrixcloud/crawlconfigs.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
ConfigRevision,
2626
CrawlConfig,
2727
CrawlConfigOut,
28+
CrawlConfigTags,
2829
CrawlOut,
2930
UpdateCrawlConfig,
3031
Organization,
@@ -976,8 +977,20 @@ async def remove_collection_from_all_configs(
976977

977978
async def get_crawl_config_tags(self, org):
978979
"""get distinct tags from all crawl configs for this org"""
979-
tags = await self.crawl_configs.distinct("tags", {"oid": org.id})
980-
return list(tags)
980+
return await self.crawl_configs.distinct("tags", {"oid": org.id})
981+
982+
async def get_crawl_config_tag_counts(self, org):
983+
"""get distinct tags from all crawl configs for this org"""
984+
tags = await self.crawl_configs.aggregate(
985+
[
986+
{"$match": {"oid": org.id}},
987+
{"$unwind": "$tags"},
988+
{"$group": {"_id": "$tags", "count": {"$sum": 1}}},
989+
{"$project": {"tag": "$_id", "count": "$count", "_id": 0}},
990+
{"$sort": {"count": -1, "tag": 1}},
991+
]
992+
).to_list()
993+
return tags
981994

982995
async def get_crawl_config_search_values(self, org):
983996
"""List unique names, first seeds, and descriptions from all workflows in org"""
@@ -1399,10 +1412,17 @@ async def get_crawl_configs(
13991412
)
14001413
return paginated_format(crawl_configs, total, page, pageSize)
14011414

1402-
@router.get("/tags", response_model=List[str])
1415+
@router.get("/tags", response_model=List[str], deprecated=True)
14031416
async def get_crawl_config_tags(org: Organization = Depends(org_viewer_dep)):
1417+
"""
1418+
Deprecated - prefer /api/orgs/{oid}/crawlconfigs/tagCounts instead.
1419+
"""
14041420
return await ops.get_crawl_config_tags(org)
14051421

1422+
@router.get("/tagCounts", response_model=CrawlConfigTags)
1423+
async def get_crawl_config_tag_counts(org: Organization = Depends(org_viewer_dep)):
1424+
return {"tags": await ops.get_crawl_config_tag_counts(org)}
1425+
14061426
@router.get("/search-values", response_model=CrawlConfigSearchValues)
14071427
async def get_crawl_config_search_values(
14081428
org: Organization = Depends(org_viewer_dep),

backend/btrixcloud/models.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,11 +577,19 @@ class CrawlConfigAddedResponse(BaseModel):
577577
execMinutesQuotaReached: bool
578578

579579

580+
# ============================================================================
581+
class CrawlConfigTagCount(BaseModel):
582+
"""Response model for crawlconfig tag count"""
583+
584+
tag: str
585+
count: int
586+
587+
580588
# ============================================================================
581589
class CrawlConfigTags(BaseModel):
582590
"""Response model for crawlconfig tags"""
583591

584-
tags: List[str]
592+
tags: List[CrawlConfigTagCount]
585593

586594

587595
# ============================================================================

backend/test/test_crawl_config_tags.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@ def test_get_config_by_tag_1(admin_auth_headers, default_org_id):
5050
assert sorted(data) == ["tag-1", "tag-2", "wr-test-1", "wr-test-2"]
5151

5252

53+
def test_get_config_by_tag_counts_1(admin_auth_headers, default_org_id):
54+
r = requests.get(
55+
f"{API_PREFIX}/orgs/{default_org_id}/crawlconfigs/tagCounts",
56+
headers=admin_auth_headers,
57+
)
58+
data = r.json()
59+
assert data == {
60+
"tags": [
61+
{"tag": "wr-test-2", "count": 2},
62+
{"tag": "tag-1", "count": 1},
63+
{"tag": "tag-2", "count": 1},
64+
{"tag": "wr-test-1", "count": 1},
65+
]
66+
}
67+
68+
5369
def test_create_new_config_2(admin_auth_headers, default_org_id):
5470
r = requests.post(
5571
f"{API_PREFIX}/orgs/{default_org_id}/crawlconfigs/",
@@ -84,6 +100,24 @@ def test_get_config_by_tag_2(admin_auth_headers, default_org_id):
84100
]
85101

86102

103+
def test_get_config_by_tag_counts_2(admin_auth_headers, default_org_id):
104+
r = requests.get(
105+
f"{API_PREFIX}/orgs/{default_org_id}/crawlconfigs/tagCounts",
106+
headers=admin_auth_headers,
107+
)
108+
data = r.json()
109+
assert data == {
110+
"tags": [
111+
{"tag": "wr-test-2", "count": 2},
112+
{"tag": "tag-0", "count": 1},
113+
{"tag": "tag-1", "count": 1},
114+
{"tag": "tag-2", "count": 1},
115+
{"tag": "tag-3", "count": 1},
116+
{"tag": "wr-test-1", "count": 1},
117+
]
118+
}
119+
120+
87121
def test_get_config_2(admin_auth_headers, default_org_id):
88122
r = requests.get(
89123
f"{API_PREFIX}/orgs/{default_org_id}/crawlconfigs/{new_cid_2}",

frontend/src/components/ui/badge.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type BadgeVariant =
1111
| "danger"
1212
| "neutral"
1313
| "primary"
14-
| "blue"
14+
| "cyan"
1515
| "high-contrast";
1616

1717
/**
@@ -27,6 +27,12 @@ export class Badge extends TailwindElement {
2727
@property({ type: String })
2828
variant: BadgeVariant = "neutral";
2929

30+
@property({ type: Boolean })
31+
outline = false;
32+
33+
@property({ type: Boolean })
34+
pill = false;
35+
3036
@property({ type: String, reflect: true })
3137
role: string | null = "status";
3238

@@ -40,16 +46,32 @@ export class Badge extends TailwindElement {
4046
return html`
4147
<span
4248
class=${clsx(
43-
tw`h-4.5 inline-flex items-center justify-center rounded-sm px-2 align-[1px] text-xs`,
44-
{
45-
success: tw`bg-success-500 text-neutral-0`,
46-
warning: tw`bg-warning-600 text-neutral-0`,
47-
danger: tw`bg-danger-500 text-neutral-0`,
48-
neutral: tw`bg-neutral-100 text-neutral-600`,
49-
"high-contrast": tw`bg-neutral-600 text-neutral-0`,
50-
primary: tw`bg-primary text-neutral-0`,
51-
blue: tw`bg-cyan-50 text-cyan-600`,
52-
}[this.variant],
49+
tw`inline-flex h-[1.125rem] items-center justify-center align-[1px] text-xs`,
50+
this.outline
51+
? [
52+
tw`ring-1`,
53+
{
54+
success: tw`bg-success-500 text-success-500 ring-success-500`,
55+
warning: tw`bg-warning-600 text-warning-600 ring-warning-600`,
56+
danger: tw`bg-danger-500 text-danger-500 ring-danger-500`,
57+
neutral: tw`g-neutral-100 text-neutral-600 ring-neutral-600`,
58+
"high-contrast": tw`bg-neutral-600 text-neutral-0 ring-neutral-0`,
59+
primary: tw`bg-white text-primary ring-primary`,
60+
cyan: tw`bg-cyan-50 text-cyan-600 ring-cyan-600`,
61+
blue: tw`bg-blue-50 text-blue-600 ring-blue-600`,
62+
}[this.variant],
63+
]
64+
: {
65+
success: tw`bg-success-500 text-neutral-0`,
66+
warning: tw`bg-warning-600 text-neutral-0`,
67+
danger: tw`bg-danger-500 text-neutral-0`,
68+
neutral: tw`bg-neutral-100 text-neutral-600`,
69+
"high-contrast": tw`bg-neutral-600 text-neutral-0`,
70+
primary: tw`bg-primary text-neutral-0`,
71+
cyan: tw`bg-cyan-50 text-cyan-600`,
72+
blue: tw`bg-blue-50 text-blue-600`,
73+
}[this.variant],
74+
this.pill ? tw`min-w-[1.125rem] rounded-full px-1` : tw`rounded px-2`,
5375
)}
5476
part="base"
5577
>

frontend/src/components/ui/tag-input.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { customElement, property, query, state } from "lit/decorators.js";
1717
import debounce from "lodash/fp/debounce";
1818

1919
import type { UnderlyingFunction } from "@/types/utils";
20+
import { type WorkflowTag } from "@/types/workflow";
2021
import { dropdown } from "@/utils/css";
2122

2223
export type Tags = string[];
@@ -80,7 +81,7 @@ export class TagInput extends LitElement {
8081
}
8182
8283
sl-popup::part(popup) {
83-
z-index: 3;
84+
z-index: 5;
8485
}
8586
8687
.shake {
@@ -116,7 +117,7 @@ export class TagInput extends LitElement {
116117
initialTags?: Tags;
117118

118119
@property({ type: Array })
119-
tagOptions: Tags = [];
120+
tagOptions: WorkflowTag[] = [];
120121

121122
@property({ type: Boolean })
122123
disabled = false;
@@ -224,6 +225,7 @@ export class TagInput extends LitElement {
224225
@paste=${this.onPaste}
225226
?required=${this.required && !this.tags.length}
226227
placeholder=${placeholder}
228+
autocomplete="off"
227229
role="combobox"
228230
aria-controls="dropdown"
229231
aria-expanded="${this.dropdownIsOpen === true}"
@@ -258,10 +260,14 @@ export class TagInput extends LitElement {
258260
>
259261
${this.tagOptions
260262
.slice(0, 3)
263+
.filter(({ tag }) => !this.tags.includes(tag))
261264
.map(
262-
(tag) => html`
265+
({ tag, count }) => html`
263266
<sl-menu-item role="option" value=${tag}
264-
>${tag}</sl-menu-item
267+
>${tag}
268+
<btrix-badge pill variant="cyan" slot="suffix"
269+
>${count}</btrix-badge
270+
></sl-menu-item
265271
>
266272
`,
267273
)}

frontend/src/features/archived-items/file-uploader.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
TagsChangeEvent,
1818
} from "@/components/ui/tag-input";
1919
import { type CollectionsChangeEvent } from "@/features/collections/collections-add";
20+
import { type WorkflowTag, type WorkflowTags } from "@/types/workflow";
2021
import { APIError } from "@/utils/api";
2122
import { maxLengthValidator } from "@/utils/form";
2223

@@ -70,7 +71,7 @@ export class FileUploader extends BtrixElement {
7071
private collectionIds: string[] = [];
7172

7273
@state()
73-
private tagOptions: Tags = [];
74+
private tagOptions: WorkflowTag[] = [];
7475

7576
@state()
7677
private tagsToSave: Tags = [];
@@ -85,7 +86,8 @@ export class FileUploader extends BtrixElement {
8586
private readonly form!: Promise<HTMLFormElement>;
8687

8788
// For fuzzy search:
88-
private readonly fuse = new Fuse([], {
89+
private readonly fuse = new Fuse<WorkflowTag>([], {
90+
keys: ["tag"],
8991
shouldSort: false,
9092
threshold: 0.2, // stricter; default is 0.6
9193
});
@@ -361,8 +363,8 @@ export class FileUploader extends BtrixElement {
361363

362364
private async fetchTags() {
363365
try {
364-
const tags = await this.api.fetch<never>(
365-
`/orgs/${this.orgId}/crawlconfigs/tags`,
366+
const { tags } = await this.api.fetch<WorkflowTags>(
367+
`/orgs/${this.orgId}/crawlconfigs/tagCounts`,
366368
);
367369

368370
// Update search/filter collection

frontend/src/features/archived-items/item-metadata-editor.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
} from "@/components/ui/tag-input";
1111
import { type CollectionsChangeEvent } from "@/features/collections/collections-add";
1212
import type { ArchivedItem } from "@/types/crawler";
13+
import { type WorkflowTag, type WorkflowTags } from "@/types/workflow";
1314
import { maxLengthValidator } from "@/utils/form";
1415
import LiteElement, { html } from "@/utils/LiteElement";
1516

@@ -46,7 +47,7 @@ export class CrawlMetadataEditor extends LiteElement {
4647
private includeName = false;
4748

4849
@state()
49-
private tagOptions: Tags = [];
50+
private tagOptions: WorkflowTag[] = [];
5051

5152
@state()
5253
private tagsToSave: Tags = [];
@@ -55,7 +56,8 @@ export class CrawlMetadataEditor extends LiteElement {
5556
private collectionsToSave: string[] = [];
5657

5758
// For fuzzy search:
58-
private readonly fuse = new Fuse<string>([], {
59+
private readonly fuse = new Fuse<WorkflowTag>([], {
60+
keys: ["tag"],
5961
shouldSort: false,
6062
threshold: 0.2, // stricter; default is 0.6
6163
});
@@ -164,8 +166,8 @@ export class CrawlMetadataEditor extends LiteElement {
164166
private async fetchTags() {
165167
if (!this.crawl) return;
166168
try {
167-
const tags = await this.apiFetch<string[]>(
168-
`/orgs/${this.crawl.oid}/crawlconfigs/tags`,
169+
const { tags } = await this.apiFetch<WorkflowTags>(
170+
`/orgs/${this.crawl.oid}/crawlconfigs/tagCounts`,
169171
);
170172

171173
// Update search/filter collection

frontend/src/features/crawl-workflows/workflow-editor.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ import {
8888
type WorkflowParams,
8989
} from "@/types/crawler";
9090
import type { UnderlyingFunction } from "@/types/utils";
91-
import { NewWorkflowOnlyScopeType } from "@/types/workflow";
91+
import {
92+
NewWorkflowOnlyScopeType,
93+
type WorkflowTag,
94+
type WorkflowTags,
95+
} from "@/types/workflow";
9296
import { track } from "@/utils/analytics";
9397
import { isApiError, isApiErrorDetail } from "@/utils/api";
9498
import { DEPTH_SUPPORTED_SCOPES, isPageScopeType } from "@/utils/crawler";
@@ -258,7 +262,7 @@ export class WorkflowEditor extends BtrixElement {
258262
private showCrawlerChannels = false;
259263

260264
@state()
261-
private tagOptions: string[] = [];
265+
private tagOptions: WorkflowTag[] = [];
262266

263267
@state()
264268
private isSubmitting = false;
@@ -293,7 +297,8 @@ export class WorkflowEditor extends BtrixElement {
293297
});
294298

295299
// For fuzzy search:
296-
private readonly fuse = new Fuse<string>([], {
300+
private readonly fuse = new Fuse<WorkflowTag>([], {
301+
keys: ["tag"],
297302
shouldSort: false,
298303
threshold: 0.2, // stricter; default is 0.6
299304
});
@@ -2532,8 +2537,8 @@ https://archiveweb.page/images/${"logo.svg"}`}
25322537
private async fetchTags() {
25332538
this.tagOptions = [];
25342539
try {
2535-
const tags = await this.api.fetch<string[]>(
2536-
`/orgs/${this.orgId}/crawlconfigs/tags`,
2540+
const { tags } = await this.api.fetch<WorkflowTags>(
2541+
`/orgs/${this.orgId}/crawlconfigs/tagCounts`,
25372542
);
25382543

25392544
// Update search/filter collection

0 commit comments

Comments
 (0)