/* eslint-disable class-methods-use-this */ /* eslint-disable max-classes-per-file */ /* eslint-disable sonarjs/no-collapsible-if */ /* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable no-nested-ternary */ import './QuerySearch.styles.scss'; import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons'; import { autocompletion, CompletionContext, CompletionResult, startCompletion, } from '@codemirror/autocomplete'; import { javascript } from '@codemirror/lang-javascript'; import { ViewPlugin, ViewUpdate } from '@codemirror/view'; import { copilot } from '@uiw/codemirror-theme-copilot'; import CodeMirror, { EditorView, Extension } from '@uiw/react-codemirror'; import { Card, Collapse, Space, Tag, Typography } from 'antd'; import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion'; import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions'; import { useCallback, useEffect, useRef, useState } from 'react'; import { IDetailedError, IQueryContext, IValidationResult, } from 'types/antlrQueryTypes'; import { QueryKeySuggestionsProps } from 'types/api/querySuggestions/types'; import { queryOperatorSuggestions, validateQuery } from 'utils/antlrQueryUtils'; import { getQueryContextAtCursor } from 'utils/queryContextUtils'; const { Text } = Typography; const { Panel } = Collapse; const queryExamples = [ { label: 'Basic Query', query: "status = 'error'", description: 'Find all errors', }, { label: 'Multiple Conditions', query: "status = 'error' AND service = 'frontend'", description: 'Find errors from frontend service', }, { label: 'IN Operator', query: "status IN ['error', 'warning']", description: 'Find items with specific statuses', }, { label: 'Function Usage', query: "HAS(service, 'frontend')", description: 'Use HAS function', }, { label: 'Numeric Comparison', query: 'duration > 1000', description: 'Find items with duration greater than 1000ms', }, { label: 'Range Query', query: 'duration BETWEEN 100 AND 1000', description: 'Find items with duration between 100ms and 1000ms', }, { label: 'Pattern Matching', query: "service LIKE 'front%'", description: 'Find services starting with "front"', }, { label: 'Complex Conditions', query: "(status = 'error' OR status = 'warning') AND service = 'frontend'", description: 'Find errors or warnings from frontend service', }, { label: 'Multiple Functions', query: "HAS(service, 'frontend') AND HAS(status, 'error')", description: 'Use multiple HAS functions', }, { label: 'NOT Operator', query: "NOT status = 'success'", description: 'Find items that are not successful', }, { label: 'Array Contains', query: "tags CONTAINS 'production'", description: 'Find items with production tag', }, { label: 'Regex Pattern', query: "service REGEXP '^prod-.*'", description: 'Find services matching regex pattern', }, { label: 'Null Check', query: 'error IS NULL', description: 'Find items without errors', }, { label: 'Multiple Attributes', query: "service = 'frontend' AND environment = 'production' AND status = 'error'", description: 'Find production frontend errors', }, { label: 'Nested Conditions', query: "(service = 'frontend' OR service = 'backend') AND (status = 'error' OR status = 'warning')", description: 'Find errors or warnings from frontend or backend', }, ]; // Custom extension to stop events const stopEventsExtension = EditorView.domEventHandlers({ keydown: (event) => { event.stopPropagation(); // Optionally: event.preventDefault(); return false; // Important for CM to know you handled it }, input: (event) => { event.stopPropagation(); return false; }, }); // Custom extension to analyze the context at the cursor position const contextAwarePlugin = ( analyzeContext: (view: EditorView, pos: number) => void, ): ViewPlugin<{ update: (update: ViewUpdate) => void }> => ViewPlugin.fromClass( class { constructor(view: EditorView) { this.analyze(view); } update(update: ViewUpdate): void { if (update.selectionSet && !update.docChanged) { this.analyze(update.view); } } analyze(view: EditorView): void { const pos = view.state.selection.main.head; analyzeContext(view, pos); } }, ); const disallowMultipleSpaces: Extension = EditorView.inputHandler.of( (view, from, to, text) => { const currentLine = view.state.doc.lineAt(from); const before = currentLine.text.slice(0, from - currentLine.from); const after = currentLine.text.slice(to - currentLine.from); const newText = before + text + after; return /\s{2,}/.test(newText); }, ); function QuerySearch(): JSX.Element { const [query, setQuery] = useState(''); 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, message: '', errors: [], }); const [keySuggestions, setKeySuggestions] = useState< QueryKeySuggestionsProps[] | null >(null); const [showExamples] = useState(false); const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 }); const lastPosRef = useRef<{ line: number; ch: number }>({ line: 0, ch: 0 }); // Reference to the editor view for programmatic autocompletion const editorRef = useRef(null); const lastKeyRef = useRef(''); const isMountedRef = useRef(true); const { data: queryKeySuggestions } = useGetQueryKeySuggestions({ signal: 'traces', }); // Add a state for tracking editing mode const [editingMode, setEditingMode] = useState< | 'key' | 'operator' | 'value' | 'conjunction' | 'function' | 'parenthesis' | 'bracketList' | null >(null); // Helper function to wrap string values in quotes if they aren't already quoted const wrapStringValueInQuotes = (value: string): string => { // If value is already quoted (with single quotes), return as is if (/^'.*'$/.test(value)) { return value; } // If value contains single quotes, escape them and wrap in single quotes if (value.includes("'")) { // Replace single quotes with escaped single quotes const escapedValue = value.replace(/'/g, "\\'"); return `'${escapedValue}'`; } // Otherwise, simply wrap in single quotes return `'${value}'`; }; // Helper function to check if operator is for list operations (IN, NOT IN, etc.) const isListOperator = (op: string | undefined): boolean => { if (!op) return false; return op.toUpperCase() === 'IN' || op.toUpperCase() === 'NOT IN'; }; // Helper function to format value based on operator type and value type const formatValueForOperator = ( value: string, operatorToken: string | undefined, type: string, ): string => { // If operator requires a list and value isn't already in list format if (isListOperator(operatorToken) && !value.startsWith('[')) { // For string values, wrap in quotes first, then in brackets if (type === 'value' || type === 'keyword') { const quotedValue = wrapStringValueInQuotes(value); return `[${quotedValue}]`; } // For numbers, just wrap in brackets return `[${value}]`; } // If we're already inside bracket list for IN operator and it's a string value // just wrap in quotes but not brackets (we're already in brackets) if (type === 'value' || type === 'keyword') { return wrapStringValueInQuotes(value); } return value; }; const analyzeContext = useCallback((view: EditorView, pos: number): void => { // Skip if component unmounted if (!isMountedRef.current) return; const doc = view.state.doc.toString(); // Check for spaces around the cursor position for debugging const isCursorAtSpace = pos < doc.length && doc[pos] === ' '; const isCursorAfterSpace = pos > 0 && doc[pos - 1] === ' '; const isCursorAfterToken = pos > 0 && doc[pos - 1] !== ' ' && doc[pos - 1] !== undefined; const isCursorBeforeToken = pos < doc.length && doc[pos] !== ' ' && doc[pos] !== undefined; // Check brackets around cursor const isCursorAtOpenBracket = pos < doc.length && (doc[pos] === '[' || doc[pos] === '('); const isCursorAfterOpenBracket = pos > 0 && (doc[pos - 1] === '[' || doc[pos - 1] === '('); const isCursorAtCloseBracket = pos < doc.length && (doc[pos] === ']' || doc[pos] === ')'); const isCursorAfterCloseBracket = pos > 0 && (doc[pos - 1] === ']' || doc[pos - 1] === ')'); // Check if cursor is at transition point (right after a token at the beginning of a space) const isTransitionPoint = isCursorAtSpace && isCursorAfterToken; // Get a slice of the text around cursor for context const sliceStart = Math.max(0, pos - 10); const sliceEnd = Math.min(doc.length, pos + 10); const textSlice = doc.substring(sliceStart, sliceEnd); const cursorPosInSlice = pos - sliceStart; // Create a visual cursor indicator const beforeCursor = textSlice.substring(0, cursorPosInSlice); const afterCursor = textSlice.substring(cursorPosInSlice); const visualCursor = `${beforeCursor}|${afterCursor}`; const context = getQueryContextAtCursor(doc, pos); // Enhanced debug logging with space and pair detection console.log('Context at cursor:', { position: pos, visualCursor, cursorAtSpace: isCursorAtSpace, cursorAfterSpace: isCursorAfterSpace, cursorAfterToken: isCursorAfterToken, cursorBeforeToken: isCursorBeforeToken, isTransitionPoint, bracketInfo: { cursorAtOpenBracket: isCursorAtOpenBracket, cursorAfterOpenBracket: isCursorAfterOpenBracket, cursorAtCloseBracket: isCursorAtCloseBracket, cursorAfterCloseBracket: isCursorAfterCloseBracket, isInBracketList: context.isInBracketList, }, contextType: context.isInKey ? 'Key' : context.isInOperator ? 'Operator' : context.isInValue ? 'Value' : context.isInConjunction ? 'Conjunction' : context.isInFunction ? 'Function' : context.isInParenthesis ? 'Parenthesis' : context.isInBracketList ? 'BracketList' : 'Unknown', keyToken: context.keyToken, operatorToken: context.operatorToken, valueToken: context.valueToken, queryPairs: context.queryPairs?.length || 0, currentPair: context.currentPair ? { key: context.currentPair.key, operator: context.currentPair.operator, value: context.currentPair.value, isComplete: context.currentPair.isComplete, } : null, }); }, []); // Add cleanup effect to prevent component updates after unmount useEffect( (): (() => void) => (): void => { // Mark component as unmounted to prevent state updates isMountedRef.current = false; }, [], ); // Use callback to prevent dependency changes on each render const fetchValueSuggestions = useCallback( async (key: string): Promise => { if ( !key || (key === activeKey && !isLoadingSuggestions) || !isMountedRef.current ) return; // Set loading state and store the key we're fetching for setIsLoadingSuggestions(true); lastKeyRef.current = key; setActiveKey(key); console.log('fetching suggestions for key:', key); // Replace current suggestions with loading indicator setValueSuggestions([ { label: 'Loading suggestions...', type: 'text', boost: -99, // Lower boost to appear at the bottom apply: (): boolean => false, // Prevent selection }, ]); try { const response = await getValueSuggestions({ key, signal: 'traces', }); // Skip updates if component unmounted or key changed if (!isMountedRef.current || lastKeyRef.current !== key) { return; // Skip updating if key has changed or component unmounted } // 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 - explicitly handle empty strings const stringOptions = stringValues // Strict filtering for empty string - we'll handle it as a special case if needed .filter( (value: string | null | undefined): value is string => value !== null && value !== undefined && value !== '', ) .map((value: string) => ({ label: value, type: 'value', })); // Generate options from number values const numberOptions = numberValues .filter( (value: number | null | undefined): value is number => value !== null && value !== undefined, ) .map((value: number) => ({ label: value.toString(), type: 'number', })); // Combine all options and make sure we don't have duplicate labels let allOptions = [...stringOptions, ...numberOptions]; // Remove duplicates by label allOptions = allOptions.filter( (option, index, self) => index === self.findIndex((o) => o.label === option.label), ); // Only if we're still on the same key if (lastKeyRef.current === key && isMountedRef.current) { if (allOptions.length > 0) { setValueSuggestions(allOptions); } else { setValueSuggestions([ { label: 'No suggestions available', type: 'text', boost: -99, // Lower boost to appear at the bottom apply: (): boolean => false, // Prevent selection }, ]); } // Force reopen the completion if editor is available if (editorRef.current) { setTimeout(() => { if (isMountedRef.current && editorRef.current) { startCompletion(editorRef.current); } }, 10); } setIsLoadingSuggestions(false); } } catch (error) { console.error('Error fetching suggestions:', error); if (lastKeyRef.current === key && isMountedRef.current) { setValueSuggestions([ { label: 'Error loading suggestions', type: 'text', boost: -99, // Lower boost to appear at the bottom apply: (): boolean => false, // Prevent selection }, ]); setIsLoadingSuggestions(false); } } }, [activeKey, isLoadingSuggestions], ); // Enhanced update handler to track context changes, including bracket contexts const handleUpdate = useCallback( (viewUpdate: { view: EditorView }): void => { // Skip updates if component is unmounted if (!isMountedRef.current) return; // Store editor reference if (!editorRef.current) { editorRef.current = viewUpdate.view; } const selection = viewUpdate.view.state.selection.main; const pos = selection.head; const doc = viewUpdate.view.state.doc.toString(); const lineInfo = viewUpdate.view.state.doc.lineAt(pos); const newPos = { line: lineInfo.number, ch: pos - lineInfo.from, }; const lastPos = lastPosRef.current; // Only update if cursor position actually changed if (newPos.line !== lastPos.line || newPos.ch !== lastPos.ch) { setCursorPos(newPos); lastPosRef.current = newPos; // Detect if cursor is at a space or after a token const isAtSpace = pos < doc.length && doc[pos] === ' '; const isAfterToken = pos > 0 && doc[pos - 1] !== ' ' && doc[pos - 1] !== undefined; const isTransitionPoint = isAtSpace && isAfterToken; // Detect brackets around cursor const isAtOpenBracket = pos < doc.length && (doc[pos] === '[' || doc[pos] === '('); const isAfterOpenBracket = pos > 0 && (doc[pos - 1] === '[' || doc[pos - 1] === '('); const isAtCloseBracket = pos < doc.length && (doc[pos] === ']' || doc[pos] === ')'); const isAfterCloseBracket = pos > 0 && (doc[pos - 1] === ']' || doc[pos - 1] === ')'); // Get context immediately when cursor position changes if (doc) { const context = getQueryContextAtCursor(doc, pos); // Only update context and mode if they've actually changed // This prevents unnecessary re-renders const previousContextType = queryContext?.isInKey ? 'key' : queryContext?.isInOperator ? 'operator' : queryContext?.isInValue ? 'value' : queryContext?.isInConjunction ? 'conjunction' : queryContext?.isInFunction ? 'function' : queryContext?.isInParenthesis ? 'parenthesis' : queryContext?.isInBracketList ? 'bracketList' : null; const newContextType = context.isInKey ? 'key' : context.isInOperator ? 'operator' : context.isInValue ? 'value' : context.isInConjunction ? 'conjunction' : context.isInFunction ? 'function' : context.isInParenthesis ? 'parenthesis' : context.isInBracketList ? 'bracketList' : null; // Log context changes for debugging if (previousContextType !== newContextType) { console.log( `Context changed: ${previousContextType || 'none'} -> ${ newContextType || 'none' }`, { position: pos, isAtSpace, isAfterToken, isTransitionPoint, bracketInfo: { isAtOpenBracket, isAfterOpenBracket, isAtCloseBracket, isAfterCloseBracket, isInBracketList: context.isInBracketList, }, keyToken: context.keyToken, operatorToken: context.operatorToken, valueToken: context.valueToken, }, ); } setQueryContext(context); // Update editing mode based on context setEditingMode(newContextType); } } }, [queryContext], ); const handleQueryChange = useCallback(async (newQuery: string) => { setQuery(newQuery); try { const validationResponse = validateQuery(newQuery); setValidation(validationResponse); } catch (error) { setValidation({ isValid: false, message: 'Failed to process query', errors: [error as IDetailedError], }); } }, []); const handleChange = (value: string): void => { setQuery(value); handleQueryChange(value); }; const handleExampleClick = (exampleQuery: string): void => { // If there's an existing query, append the example with AND const newQuery = query ? `${query} AND ${exampleQuery}` : exampleQuery; setQuery(newQuery); handleQueryChange(newQuery); }; // Helper function to render a badge for the current context mode const renderContextBadge = (): JSX.Element => { if (!editingMode) return Unknown; switch (editingMode) { case 'key': return Key; case 'operator': return Operator; case 'value': return Value; case 'conjunction': return Conjunction; case 'function': return Function; case 'parenthesis': return Parenthesis; case 'bracketList': return Bracket List; default: return Unknown; } }; // Enhanced myCompletions function to better use context including query pairs function myCompletions(context: CompletionContext): CompletionResult | null { const word = context.matchBefore(/[.\w]*/); if (word?.from === word?.to && !context.explicit) return null; // Get the query context at the cursor position const queryContext = getQueryContextAtCursor(query, cursorPos.ch); // Define autocomplete options based on the context let options: { label: string; type: string; info?: string; apply?: string; detail?: string; boost?: number; }[] = []; // Special handling for bracket list context (for IN operator) if (queryContext.isInBracketList) { // If we're inside brackets for an IN operator, we want to show value suggestions // but format them differently (just add quotes, don't wrap in brackets) const keyName = queryContext.keyToken || queryContext.currentPair?.key || ''; if (!keyName) { return null; } // Trigger fetch only if needed if (keyName && (keyName !== activeKey || isLoadingSuggestions)) { // Don't trigger a new fetch if we're already loading for this key if (!(isLoadingSuggestions && lastKeyRef.current === keyName)) { fetchValueSuggestions(keyName); } } // For values in bracket list, just add quotes without enclosing in brackets const processedOptions = valueSuggestions.map((option) => { // Clone the option to avoid modifying the original const processedOption = { ...option }; // Skip processing for non-selectable items if (option.apply === false || typeof option.apply === 'function') { return option; } // For strings, just wrap in quotes (no brackets needed) if (option.type === 'value' || option.type === 'keyword') { processedOption.apply = wrapStringValueInQuotes(option.label); processedOption.info = `Value for ${keyName} IN list`; } else { processedOption.apply = option.label; processedOption.info = `Value for ${keyName} IN list`; } return processedOption; }); // Return current value suggestions without comma return { from: word?.from ?? 0, options: processedOptions, }; } if (queryContext.isInKey) { const searchText = word?.text.toLowerCase() ?? ''; options = (keySuggestions || []).filter((option) => option.label.toLowerCase().includes(searchText), ); // If we have previous pairs, we can prioritize keys that haven't been used yet if (queryContext.queryPairs && queryContext.queryPairs.length > 0) { const usedKeys = queryContext.queryPairs.map((pair) => pair.key); // Add boost to unused keys to prioritize them options = options.map((option) => ({ ...option, boost: usedKeys.includes(option.label) ? -10 : 10, info: usedKeys.includes(option.label) ? `${option.info || ''} (already used in query)` : option.info, })); } // Add boost to exact matches options = options.map((option) => ({ ...option, boost: (option.boost || 0) + (option.label.toLowerCase() === searchText ? 100 : 0), })); return { from: word?.from ?? 0, to: word?.to ?? cursorPos.ch, options, }; } if (queryContext.isInOperator) { options = queryOperatorSuggestions; // Get key information from context or current pair const keyName = queryContext.keyToken || queryContext.currentPair?.key; // If we have a key context, add that info to the operator suggestions if (keyName) { // Find the key details from suggestions const keyDetails = (keySuggestions || []).find((k) => k.label === keyName); const keyType = keyDetails?.type || ''; // Filter operators based on key type if (keyType) { if (keyType === 'number') { // Prioritize numeric operators options = options.map((op) => ({ ...op, boost: ['>', '<', '>=', '<=', '=', '!=', 'BETWEEN'].includes(op.label) ? 100 : 0, })); } else if (keyType === 'string' || keyType === 'keyword') { // Prioritize string operators options = options.map((op) => ({ ...op, boost: ['=', '!=', 'LIKE', 'ILIKE', 'CONTAINS', 'IN'].includes(op.label) ? 100 : 0, })); } else if (keyType === 'boolean') { // Prioritize boolean operators options = options.map((op) => ({ ...op, boost: ['=', '!='].includes(op.label) ? 100 : 0, })); } } // Add key info to all operators options = options.map((op) => ({ ...op, info: `${op.info || ''} (for ${keyName})`, })); } return { from: word?.from ?? 0, to: word?.to ?? cursorPos.ch, options, }; } if (queryContext.isInValue) { // Fetch values based on the key - use available context const keyName = queryContext.keyToken || queryContext.currentPair?.key || ''; const operatorName = queryContext.operatorToken || queryContext.currentPair?.operator || ''; if (!keyName) { return null; } // Trigger fetch only if needed if (keyName && (keyName !== activeKey || isLoadingSuggestions)) { // Don't trigger a new fetch if we're already loading for this key if (!(isLoadingSuggestions && lastKeyRef.current === keyName)) { fetchValueSuggestions(keyName); } } // Process options to add appropriate formatting when selected const processedOptions = valueSuggestions.map((option) => { // Clone the option to avoid modifying the original const processedOption = { ...option }; // Skip processing for non-selectable items if (option.apply === false || typeof option.apply === 'function') { return option; } // Format values based on their type and the operator if (option.type === 'value' || option.type === 'keyword') { // String values get quoted processedOption.apply = formatValueForOperator( option.label, operatorName, option.type, ); // Add context info to the suggestion if (keyName && operatorName) { processedOption.info = `Value for ${keyName} ${operatorName}`; } } else if (option.type === 'number') { // Numbers don't get quoted but may need brackets for IN operators if (isListOperator(operatorName)) { processedOption.apply = `[${option.label}]`; } else { processedOption.apply = option.label; } // Add context info to the suggestion if (keyName && operatorName) { processedOption.info = `Numeric value for ${keyName} ${operatorName}`; } } else if (option.type === 'boolean') { // Boolean values don't get quoted processedOption.apply = option.label; // Add context info if (keyName && operatorName) { processedOption.info = `Boolean value for ${keyName} ${operatorName}`; } } else if (option.type === 'array') { // Arrays are already formatted as arrays processedOption.apply = option.label; // Add context info if (keyName && operatorName) { processedOption.info = `Array value for ${keyName} ${operatorName}`; } } return processedOption; }); // Return current value suggestions from state return { from: word?.from ?? 0, options: processedOptions, }; } if (queryContext.isInFunction) { options = [ { label: 'HAS', type: 'function' }, { label: 'HASANY', type: 'function' }, { label: 'HASALL', type: 'function' }, { label: 'HASNONE', type: 'function' }, ]; return { from: word?.from ?? 0, options, }; } if (queryContext.isInConjunction) { options = [ { label: 'AND', type: 'conjunction' }, { label: 'OR', type: 'conjunction' }, ]; return { from: word?.from ?? 0, options, }; } if (queryContext.isInParenthesis) { // Different suggestions based on the context within parenthesis or bracket const curChar = query.charAt(cursorPos.ch - 1) || ''; if (curChar === '(' || curChar === '[') { // Right after opening parenthesis/bracket if (curChar === '(') { // In expression context, suggest keys, functions, or nested parentheses return { from: word?.from ?? 0, options: [ ...(keySuggestions || []), { label: '(', type: 'parenthesis', info: 'Open nested group' }, { label: 'NOT', type: 'operator', info: 'Negate expression' }, ...options.filter((opt) => opt.type === 'function'), ], }; } // Inside square brackets (likely for IN operator) // Suggest values, commas, or closing bracket return { from: word?.from ?? 0, options: valueSuggestions, }; } if (curChar === ')' || curChar === ']') { // After closing parenthesis/bracket, suggest conjunctions return { from: word?.from ?? 0, options: [ { label: 'AND', type: 'conjunction' }, { label: 'OR', type: 'conjunction' }, ], }; } } // If no specific context is detected, provide general suggestions return { from: word?.from ?? 0, options: [ ...(keySuggestions || []), { label: 'AND', type: 'conjunction', boost: -10 }, { label: 'OR', type: 'conjunction', boost: -10 }, { label: '(', type: 'parenthesis', info: 'Open group', boost: -20 }, ], }; } // 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(() => { // Skip if we don't have a value context or it hasn't changed if (!queryContext?.isInValue) return; const { keyToken, currentToken } = queryContext; const key = keyToken || currentToken; // Only fetch if needed and if we have a valid key if (key && key !== activeKey && !isLoadingSuggestions) { fetchValueSuggestions(key); } // Use only the specific properties of queryContext we need to avoid unnecessary renders }, [queryContext, activeKey, isLoadingSuggestions, fetchValueSuggestions]); return (
{/* Add a context indicator banner */} {editingMode && (
Currently editing: {renderContextBadge()} {queryContext?.keyToken && ( Key: {queryContext.keyToken} )} {queryContext?.operatorToken && ( Operator: {queryContext.operatorToken} )} {queryContext?.valueToken && ( Value: {queryContext.valueToken} )} {queryContext?.currentPair && ( Current pair: {queryContext.currentPair.key} {queryContext.currentPair.operator} {queryContext.currentPair.value && ( {queryContext.currentPair.value} )} {queryContext.currentPair.isComplete ? 'Complete' : 'Incomplete'} )} {queryContext?.queryPairs && queryContext.queryPairs.length > 0 && ( Total pairs: {queryContext.queryPairs.length} )}
)} {query && (
searchExpr {query}
Status:
{validation.isValid ? ( Valid ) : ( Invalid )}
{validation.errors.map((error) => (
{error.line}:{error.column}
{error.message}
))}
)} {showExamples && (
{queryExamples.map((example) => (
handleExampleClick(example.query)} role="button" tabIndex={0} onKeyDown={(e): void => { if (e.key === 'Enter' || e.key === ' ') { handleExampleClick(example.query); } }} >
))}
)} {/* {queryContext && (
Token: {queryContext.currentToken || '-'} Type: {queryContext.tokenType || '-'} Context: {renderContextBadge()} {queryContext.keyToken && ( Key: {queryContext.keyToken} )} {queryContext.operatorToken && ( Operator: {queryContext.operatorToken} )} {queryContext.valueToken && ( Value: {queryContext.valueToken} )}
)} */}
); } export default QuerySearch;