From 9173cb754d48bc14e7ecfb12423b8e412f840e17 Mon Sep 17 00:00:00 2001 From: Ashton Lin Date: Thu, 30 Jul 2026 00:16:03 +0800 Subject: [PATCH] feat(elasticsearch): add searchable field picker --- .../components/document/DocumentBrowser.vue | 67 ++-- .../DocumentBrowserFieldSearch.spec.ts | 289 ++++++++++++++++++ .../src/lib/app/documentStoreProvider.ts | 73 +++++ .../app-tests/documentStoreProvider.test.ts | 66 ++++ 4 files changed, 471 insertions(+), 24 deletions(-) create mode 100644 apps/desktop/src/components/document/__tests__/DocumentBrowserFieldSearch.spec.ts diff --git a/apps/desktop/src/components/document/DocumentBrowser.vue b/apps/desktop/src/components/document/DocumentBrowser.vue index 916f7a988..690ec9a80 100644 --- a/apps/desktop/src/components/document/DocumentBrowser.vue +++ b/apps/desktop/src/components/document/DocumentBrowser.vue @@ -34,10 +34,12 @@ import { documentFilterModeOptions, documentStoreProviderFor, elasticsearchBoolClauseOptions, + elasticsearchFieldPathTreeFromFieldNames, elasticsearchQueryTypeNeedsValue, elasticsearchQueryTypeOptions, elasticsearchStructuredFilter, formatDocumentQueryInput, + searchElasticsearchFieldPathTree, type DocumentFieldPathNode, type DocumentFilterMode, type DocumentFilterRule, @@ -254,7 +256,15 @@ const gridResult = computed(() => { return { columns, rows, mongo_documents: docs, mongo_copy_documents: copyDocuments.value, affected_rows: 0, execution_time_ms: 0, truncated: false }; }); const expandedDocumentFilterFieldPaths = ref>(new Set()); +const elasticsearchFieldTypes = computed(() => new Map(elasticsearchMappingFields.value.map((field) => [field.name, field.data_type]))); +const elasticsearchFilterFieldNames = computed(() => { + const names = [...elasticsearchMappingFields.value.map((field) => field.name), ...gridResult.value.columns, "_id", "_routing"]; + return [...new Set(names.filter(Boolean))]; +}); const documentFilterFieldTree = computed(() => { + if (documentStoreProvider.value.kind === "elasticsearch") { + return elasticsearchFieldPathTreeFromFieldNames(elasticsearchFilterFieldNames.value, elasticsearchFieldTypes.value); + } const tree = documentFieldPathTreeFromDocuments(documents.value); if (tree.length > 0) return tree; return gridResult.value.columns.map((column) => ({ @@ -269,15 +279,15 @@ const documentFilterFieldTree = computed(() => { }); const documentFilterFieldOptions = computed(() => { if (documentStoreProvider.value.kind === "elasticsearch") { - const names = [...elasticsearchMappingFields.value.map((field) => field.name), ...gridResult.value.columns, "_id", "_routing"]; - return [...new Set(names.filter(Boolean))]; + return flattenDocumentFieldPathTree(documentFilterFieldTree.value) + .filter((field) => field.selectable) + .map((field) => field.path); } const nestedFields = documentFieldPathOptionsFromDocuments(documents.value); return nestedFields.length > 0 ? nestedFields : gridResult.value.columns; }); const documentFilterFieldRows = computed(() => visibleDocumentFilterFieldRows(documentFilterFieldTree.value)); const documentFilterFieldByPath = computed(() => new Map(flattenDocumentFieldPathTree(documentFilterFieldTree.value).map((node) => [node.path, node]))); -const elasticsearchFieldTypes = computed(() => new Map(elasticsearchMappingFields.value.map((field) => [field.name, field.data_type]))); const documentStructuredFilterCount = computed(() => { if (!appliedDocumentFilter.value) return 0; if (documentStoreProvider.value.kind !== "elasticsearch") return 1; @@ -336,6 +346,9 @@ function setDocumentFilterFieldPopoverOpen(ruleId: string, open: boolean) { if (open) next[ruleId] = true; else delete next[ruleId]; documentFilterFieldPopoverOpen.value = next; + if (open) { + void nextTick(() => document.getElementById(documentFilterFieldSearchInputId(ruleId))?.focus()); + } if (!open) { const search = { ...documentFilterFieldSearch.value }; delete search[ruleId]; @@ -343,6 +356,10 @@ function setDocumentFilterFieldPopoverOpen(ruleId: string, open: boolean) { } } +function documentFilterFieldSearchInputId(ruleId: string): string { + return `document-filter-field-search-${ruleId}`; +} + function documentFilterFieldSearchActive(ruleId: string): boolean { return !!documentFilterFieldSearch.value[ruleId]?.trim(); } @@ -350,7 +367,8 @@ function documentFilterFieldSearchActive(ruleId: string): boolean { function documentFilterFieldRowsForRule(ruleId: string): DocumentFilterFieldTreeRow[] { const query = documentFilterFieldSearch.value[ruleId] ?? ""; if (!query.trim()) return documentFilterFieldRows.value; - return searchDocumentFieldPathTree(documentFilterFieldTree.value, query).map((node) => ({ ...node, depth: 0 })); + const matchingFields = documentStoreProvider.value.kind === "elasticsearch" ? searchElasticsearchFieldPathTree(documentFilterFieldTree.value, query) : searchDocumentFieldPathTree(documentFilterFieldTree.value, query); + return matchingFields.map((node) => ({ ...node, depth: 0 })); } function selectDocumentFilterField(ruleId: string, fieldName: string) { @@ -359,7 +377,11 @@ function selectDocumentFilterField(ruleId: string, fieldName: string) { } function documentFilterFieldLabel(path: string): string { - return documentFilterFieldByPath.value.get(path)?.displayPath ?? path; + if (documentStoreProvider.value.kind !== "elasticsearch") { + return documentFilterFieldByPath.value.get(path)?.displayPath ?? path; + } + const fieldType = elasticsearchFieldTypes.value.get(path); + return fieldType ? `${path} (${fieldType})` : path; } function documentFilterFieldKindLabel(kind: DocumentFieldPathNode["kind"]): string { @@ -1684,7 +1706,7 @@ defineExpose({ focusSearch }); - + -
@@ -1721,20 +1754,6 @@ defineExpose({ focusSearch }); - -