diff --git a/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx b/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx index 88638c7be1d5..43a91e226591 100644 --- a/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx +++ b/frontend/src/components/QueryBuilderV2/CodeMirrorWhereClause/CodeMirrorWhereClause.tsx @@ -1,3 +1,5 @@ +/* eslint-disable sonarjs/no-collapsible-if */ +/* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable no-nested-ternary */ @@ -13,11 +15,13 @@ import { autocompletion, CompletionContext, CompletionResult, + startCompletion, } from '@codemirror/autocomplete'; import { javascript } from '@codemirror/lang-javascript'; import { copilot } from '@uiw/codemirror-theme-copilot'; import CodeMirror, { EditorView, Extension } from '@uiw/react-codemirror'; import { Badge, Card, Divider, Space, Tooltip, Typography } from 'antd'; +import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion'; import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions'; // import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -85,7 +89,12 @@ function collapseSpacesOutsideStrings(): Extension { function CodeMirrorWhereClause(): JSX.Element { const [query, setQuery] = useState(''); - const [isLoading, setIsLoading] = useState(false); + const [valueSuggestions, setValueSuggestions] = useState([ + { label: 'error', type: 'value' }, + { label: 'frontend', type: 'value' }, + ]); + const [activeKey, setActiveKey] = useState(''); + const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); const [queryContext, setQueryContext] = useState(null); const [validation, setValidation] = useState({ isValid: false, @@ -100,45 +109,99 @@ function CodeMirrorWhereClause(): JSX.Element { const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 }); const lastPosRef = useRef<{ line: number; ch: number }>({ line: 0, ch: 0 }); - const { - data: queryKeySuggestions, - // isLoading: queryKeySuggestionsLoading, - // isRefetching: queryKeySuggestionsRefetching, - // refetch: queryKeySuggestionsRefetch, - // error: queryKeySuggestionsError, - // isError: queryKeySuggestionsIsError, - } = useGetQueryKeySuggestions({ signal: 'traces' }); + // Reference to the editor view for programmatic autocompletion + const editorRef = useRef(null); + const lastKeyRef = useRef(''); - // const { - // data: queryKeyValuesSuggestions, - // isLoading: queryKeyValuesSuggestionsLoading, - // refetch: refetchQueryKeyValuesSuggestions, - // } = useGetQueryKeyValueSuggestions({ - // key: 'status', - // signal: 'traces', - // }); + const { data: queryKeySuggestions } = useGetQueryKeySuggestions({ + signal: 'traces', + }); - console.log('loading', isLoading); + // Use callback to prevent dependency changes on each render + const fetchValueSuggestions = useCallback( + async (key: string): Promise => { + if (!key || (key === activeKey && !isLoadingSuggestions)) return; - const generateOptions = (data: any): any[] => - Object.values(data.keys).flatMap((items: any) => - items.map(({ name, fieldDataType, fieldContext }: any) => ({ - label: name, - type: fieldDataType === 'string' ? 'keyword' : fieldDataType, - info: fieldContext, - details: '', - })), - ); + console.log('Fetching suggestions for key:', key); - useEffect(() => { - if (queryKeySuggestions) { - const options = generateOptions(queryKeySuggestions.data.data); + // Set loading state and store the key we're fetching for + setIsLoadingSuggestions(true); + lastKeyRef.current = key; + setActiveKey(key); - setKeySuggestions(options); - } - }, [queryKeySuggestions]); + // Replace current suggestions with loading indicator + setValueSuggestions([{ label: 'Loading suggestions...', type: 'text' }]); + + try { + const response = await getValueSuggestions({ + key, + signal: 'traces', + }); + + // Verify we're still on the same key (user hasn't moved on) + if (lastKeyRef.current !== key) { + return; // Skip updating if key has changed + } + + // Process the response data + const responseData = response.data as any; + const values = responseData.data?.values || {}; + const stringValues = values.stringValues || []; + const numberValues = values.numberValues || []; + + // Generate options from string values + const stringOptions = stringValues.map((value: string) => ({ + label: value || '""', + type: 'value', + })); + + // Generate options from number values + const numberOptions = numberValues.map((value: number) => ({ + label: value.toString(), + type: 'number', + })); + + // Update suggestions + const allOptions = [...stringOptions, ...numberOptions]; + + // Only if we're still on the same key + if (lastKeyRef.current === key) { + if (allOptions.length > 0) { + setValueSuggestions(allOptions); + console.log('Updated value suggestions:', allOptions); + } else { + setValueSuggestions([ + { label: 'No suggestions available', type: 'text' }, + ]); + } + + // Force reopen the completion if editor is available + if (editorRef.current) { + setTimeout(() => { + startCompletion(editorRef.current!); + }, 10); + } + setIsLoadingSuggestions(false); + } + } catch (error) { + console.error('Error fetching suggestions:', error); + if (lastKeyRef.current === key) { + setValueSuggestions([ + { label: 'Error loading suggestions', type: 'text' }, + ]); + setIsLoadingSuggestions(false); + } + } + }, + [activeKey, isLoadingSuggestions], + ); const handleUpdate = (viewUpdate: { view: EditorView }): void => { + // Store editor reference + if (!editorRef.current) { + editorRef.current = viewUpdate.view; + } + const selection = viewUpdate.view.state.selection.main; const pos = selection.head; @@ -158,7 +221,6 @@ function CodeMirrorWhereClause(): JSX.Element { }; const handleQueryChange = useCallback(async (newQuery: string) => { - setIsLoading(true); setQuery(newQuery); try { @@ -170,8 +232,6 @@ function CodeMirrorWhereClause(): JSX.Element { message: 'Failed to process query', errors: [error instanceof Error ? error.message : 'Unknown error'], }); - } finally { - setIsLoading(false); } }, []); @@ -244,43 +304,69 @@ function CodeMirrorWhereClause(): JSX.Element { if (queryContext.isInKey) { options = keySuggestions || []; - } else if (queryContext.isInOperator) { + return { + from: word?.from ?? 0, + options, + }; + } + + if (queryContext.isInOperator) { options = queryOperatorSuggestions; - } else if (queryContext.isInValue) { + return { + from: word?.from ?? 0, + options, + }; + } + + if (queryContext.isInValue) { console.log('is In Value', queryContext.currentToken); - // refetchQueryKeyValuesSuggestions(); // Fetch values based on the key - use the keyToken if available const key = queryContext.keyToken || queryContext.currentToken; console.log('key', key); - // const response = refetchQueryKeyValuesSuggestions({ - // key: 'status', - // signal: 'traces', - // }); + // Trigger fetch only if key is different from activeKey or if we're still loading + if (key && (key !== activeKey || isLoadingSuggestions)) { + // Don't trigger a new fetch if we're already loading for this key + if (!(isLoadingSuggestions && lastKeyRef.current === key)) { + fetchValueSuggestions(key); + } + } - options = [ - { label: 'error', type: 'value' }, - { label: 'frontend', type: 'value' }, - // Add more value options here - ]; - } else if (queryContext.isInFunction) { + // Return current value suggestions from state + return { + from: word?.from ?? 0, + options: valueSuggestions, + }; + } + + if (queryContext.isInFunction) { options = [ { label: 'HAS', type: 'function' }, { label: 'HASANY', type: 'function' }, // Add more function options here ]; - } else if (queryContext.isInConjunction) { + return { + from: word?.from ?? 0, + options, + }; + } + + if (queryContext.isInConjunction) { options = [ { label: 'AND', type: 'conjunction' }, { label: 'OR', type: 'conjunction' }, ]; + return { + from: word?.from ?? 0, + options, + }; } return { from: word?.from ?? 0, - options, + options: [], }; } @@ -311,6 +397,37 @@ function CodeMirrorWhereClause(): JSX.Element { }, }); + // Add back the generateOptions function and useEffect + const generateOptions = (data: any): any[] => + Object.values(data.keys).flatMap((items: any) => + items.map(({ name, fieldDataType, fieldContext }: any) => ({ + label: name, + type: fieldDataType === 'string' ? 'keyword' : fieldDataType, + info: fieldContext, + details: '', + })), + ); + + useEffect(() => { + if (queryKeySuggestions) { + const options = generateOptions(queryKeySuggestions.data.data); + setKeySuggestions(options); + } + }, [queryKeySuggestions]); + + // Update state when query context changes to trigger suggestion refresh + useEffect(() => { + if (queryContext?.isInValue) { + const key = queryContext.keyToken || queryContext.currentToken; + if (key && (key !== activeKey || isLoadingSuggestions)) { + // Don't trigger a new fetch if we're already loading for this key + if (!(isLoadingSuggestions && lastKeyRef.current === key)) { + fetchValueSuggestions(key); + } + } + } + }, [queryContext, activeKey, fetchValueSuggestions, isLoadingSuggestions]); + return (