diff --git a/changelogs/fragments/10901.yml b/changelogs/fragments/10901.yml new file mode 100644 index 000000000000..5272b60dda7d --- /dev/null +++ b/changelogs/fragments/10901.yml @@ -0,0 +1,2 @@ +fix: +- First character being trimmed during Query Snippet Insertion ([#10901](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10901)) \ No newline at end of file diff --git a/src/plugins/data/public/query_snippet_suggestions/ppl/suggestions.test.ts b/src/plugins/data/public/query_snippet_suggestions/ppl/suggestions.test.ts index 0131c3ad1d3e..ebb46a50a60b 100644 --- a/src/plugins/data/public/query_snippet_suggestions/ppl/suggestions.test.ts +++ b/src/plugins/data/public/query_snippet_suggestions/ppl/suggestions.test.ts @@ -79,8 +79,8 @@ describe('PPL Query Snippet Suggestions', () => { }); describe('convertQueryToMonacoSuggestion', () => { - it('should convert queries to Monaco suggestions without user query', () => { - const result = convertQueryToMonacoSuggestion(mockQueries, ''); + it('should convert queries to Monaco suggestions', () => { + const result = convertQueryToMonacoSuggestion(mockQueries); expect(result).toHaveLength(9); // 3 queries with 3, 3, 3 segments each @@ -98,8 +98,8 @@ describe('PPL Query Snippet Suggestions', () => { ); }); - it('should convert queries to Monaco suggestions with user query', () => { - const result = convertQueryToMonacoSuggestion(mockQueries, 'source = logs | '); + it('should not include insertText in base conversion', () => { + const result = convertQueryToMonacoSuggestion(mockQueries); expect(result).toHaveLength(9); @@ -122,7 +122,7 @@ describe('PPL Query Snippet Suggestions', () => { }, ]; - const result = convertQueryToMonacoSuggestion(duplicateQueries, ''); + const result = convertQueryToMonacoSuggestion(duplicateQueries); // Should only have 2 unique suggestions (source = logs, where status = "error") expect(result).toHaveLength(2); @@ -142,7 +142,7 @@ describe('PPL Query Snippet Suggestions', () => { const whereResult = result.find((s) => s.text.startsWith('where')); expect(whereResult).toBeDefined(); - expect(whereResult?.insertText).toBe('re status = "error" '); // Should complete "whe" to "where status = "error"" with trailing space + expect(whereResult?.insertText).toBe('where status = "error" '); // Should complete "whe" to "where status = "error"" with trailing space }); it('should return empty array when no matching suggestions', async () => { @@ -158,7 +158,7 @@ describe('PPL Query Snippet Suggestions', () => { const statsResult = result.find((s) => s.text.startsWith('stats')); expect(statsResult).toBeDefined(); - expect(statsResult?.insertText).toBe('ats count() by host '); + expect(statsResult?.insertText).toBe('stats count() by host '); }); it('should not return suggestions that match exactly', async () => { @@ -190,5 +190,79 @@ describe('PPL Query Snippet Suggestions', () => { expect(result).toHaveLength(0); }); + + describe('token-based insertText logic', () => { + it('should handle partial token matching correctly', async () => { + const result = await getPPLQuerySnippetForSuggestions('source = logs | wh'); + + const whereResult = result.find((s) => s.text.startsWith('where')); + expect(whereResult).toBeDefined(); + expect(whereResult?.insertText).toBe('where status = "error" '); + }); + + it('should skip fully matched tokens and insert remaining tokens', async () => { + const result = await getPPLQuerySnippetForSuggestions('source = logs | where status'); + + const whereResult = result.find((s) => s.text.startsWith('where')); + expect(whereResult).toBeDefined(); + expect(whereResult?.insertText).toBe('= "error" '); + }); + + it('should handle multiple fully matched tokens', async () => { + const result = await getPPLQuerySnippetForSuggestions('source = logs | where status ='); + + const whereResult = result.find((s) => s.text.startsWith('where')); + expect(whereResult).toBeDefined(); + expect(whereResult?.insertText).toBe('"error" '); + }); + + it('should return complete suggestion when no tokens match', async () => { + const result = await getPPLQuerySnippetForSuggestions('source = logs | xyz'); + + expect(result).toHaveLength(0); // No suggestions should match 'xyz' + }); + + it('should handle case-insensitive token matching', async () => { + const result = await getPPLQuerySnippetForSuggestions('source = logs | WHERE STATUS'); + + const whereResult = result.find((s) => s.text.startsWith('where')); + expect(whereResult).toBeDefined(); + expect(whereResult?.insertText).toBe('= "error" '); + }); + + it('should handle single character partial match', async () => { + const result = await getPPLQuerySnippetForSuggestions('source = logs | w'); + + const whereResult = result.find((s) => s.text.startsWith('where')); + expect(whereResult).toBeDefined(); + expect(whereResult?.insertText).toBe('where status = "error" '); + }); + + it('should handle exact token boundary matching', async () => { + const result = await getPPLQuerySnippetForSuggestions('source = logs | where'); + + const whereResult = result.find((s) => s.text.startsWith('where')); + expect(whereResult).toBeDefined(); + expect(whereResult?.insertText).toBe('status = "error" '); + }); + + it('should handle suggestions with different token structures', async () => { + const result = await getPPLQuerySnippetForSuggestions('source = metrics | f'); + + const fieldsResult = result.find((s) => s.text.startsWith('fields')); + expect(fieldsResult).toBeDefined(); + expect(fieldsResult?.insertText).toBe('fields timestamp, cpu_usage '); + }); + + it('should handle when suggestion has fewer tokens than user input', async () => { + // Test with a complex user query that has more tokens than any single suggestion + const result = await getPPLQuerySnippetForSuggestions( + 'source = logs | where status = "error" extra token' + ); + + // Should have no matches since no suggestion starts with this complex pattern + expect(result).toHaveLength(0); + }); + }); }); }); diff --git a/src/plugins/data/public/query_snippet_suggestions/ppl/suggestions.ts b/src/plugins/data/public/query_snippet_suggestions/ppl/suggestions.ts index 76c50fdc2f8a..ae1498e24e0d 100644 --- a/src/plugins/data/public/query_snippet_suggestions/ppl/suggestions.ts +++ b/src/plugins/data/public/query_snippet_suggestions/ppl/suggestions.ts @@ -16,10 +16,7 @@ export const extractSnippetsFromQuery = (query: string) => { return pplQuerySegments; }; -export const convertQueryToMonacoSuggestion = ( - queries: QuerySnippetItem[], - userQuery: string -): QuerySuggestion[] => { +export const convertQueryToMonacoSuggestion = (queries: QuerySnippetItem[]): QuerySuggestion[] => { const textMap = new Map(); queries.forEach((query) => { @@ -52,11 +49,12 @@ export const getPPLQuerySnippetForSuggestions = async ( // Fetfch all User Queries const userQueries = await getUserPastQueries(languageId); - const suggestions = convertQueryToMonacoSuggestion(userQueries, userQuery); + const suggestions = convertQueryToMonacoSuggestion(userQueries); // Extract the last Segment from the query const userQuerySegments = userQuery.split('|'); const currentUserQuerySegment = userQuerySegments.pop()?.trim().toLowerCase(); + const typedTokens = currentUserQuerySegment?.split(/\s+/); // Using currentUserQuery to do a prefix filtering of the Query Segments if (currentUserQuerySegment) { @@ -67,10 +65,29 @@ export const getPPLQuerySnippetForSuggestions = async ( currentUserQuerySegment !== suggestion.text ); }) - .map((suggestion) => ({ - ...suggestion, - insertText: suggestion.text.slice(currentUserQuerySegment.length).trim() + ' ', - })); + .map((suggestion) => { + const suggestionTokens = suggestion.text.split(/\s+/); + + let insertIndex = 0; + + // Skip fully matched tokens only + while ( + typedTokens && + insertIndex < typedTokens.length && + insertIndex < suggestionTokens.length && + suggestionTokens[insertIndex].toLowerCase() === typedTokens[insertIndex].toLowerCase() + ) { + insertIndex++; + } + + // Insert from the first partially matched token onwards + const remainingTokens = suggestionTokens.slice(insertIndex).join(' '); + + return { + ...suggestion, + insertText: remainingTokens + ' ', + }; + }); } return [];