/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable no-nested-ternary */ import './CodeMirrorWhereClause.styles.scss'; import { CheckCircleFilled, CloseCircleFilled, InfoCircleOutlined, QuestionCircleOutlined, } from '@ant-design/icons'; import { autocompletion, CompletionContext, CompletionResult, } 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 { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions'; // import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions'; import { useCallback, useEffect, useRef, useState } from 'react'; import { IQueryContext, IValidationResult } from 'types/antlrQueryTypes'; import { QueryKeySuggestionsProps } from 'types/api/querySuggestions/types'; import { getQueryContextAtCursor, queryOperatorSuggestions, validateQuery, } from 'utils/antlrQueryUtils'; const { Text, Title } = Typography; function collapseSpacesOutsideStrings(): Extension { return EditorView.inputHandler.of((view, from, to, text) => { // Get the current line text const { state } = view; const line = state.doc.lineAt(from); // Find the position within the line const before = line.text.slice(0, from - line.from); const after = line.text.slice(to - line.from); const fullText = before + text + after; let insideString = false; let escaped = false; let processed = ''; for (let i = 0; i < fullText.length; i++) { const char = fullText[i]; if (char === '"' && !escaped) { insideString = !insideString; } if (char === '\\' && !escaped) { escaped = true; } else { escaped = false; } if (!insideString && char === ' ' && processed.endsWith(' ')) { // Collapse multiple spaces outside strings // Skip this space } else { processed += char; } } // Only dispatch if the processed text differs if (processed !== fullText) { view.dispatch({ changes: { from: line.from, to: line.to, insert: processed, }, }); return true; } return false; }); } function CodeMirrorWhereClause(): JSX.Element { const [query, setQuery] = useState(''); const [isLoading, setIsLoading] = useState(false); const [queryContext, setQueryContext] = useState(null); const [validation, setValidation] = useState({ isValid: false, message: '', errors: [], }); const [keySuggestions, setKeySuggestions] = useState< QueryKeySuggestionsProps[] | null >(null); 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' }); // const { // data: queryKeyValuesSuggestions, // isLoading: queryKeyValuesSuggestionsLoading, // refetch: refetchQueryKeyValuesSuggestions, // } = useGetQueryKeyValueSuggestions({ // signal: 'traces', // key: 'status', // }); const generateOptions = (data: any): any[] => { const options = 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('options', options); return options; }; useEffect(() => { if (queryKeySuggestions) { console.log('queryKeySuggestions', queryKeySuggestions); const options = generateOptions(queryKeySuggestions.data.data); setKeySuggestions(options); } }, [queryKeySuggestions]); console.log('keySuggestions', keySuggestions); const handleUpdate = (viewUpdate: { view: EditorView }): void => { const selection = viewUpdate.view.state.selection.main; const pos = selection.head; 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; } }; console.log({ cursorPos, queryContext, validation, isLoading, }); const handleQueryChange = useCallback(async (newQuery: string) => { setIsLoading(true); setQuery(newQuery); try { const validationResponse = validateQuery(newQuery); setValidation(validationResponse); } catch (error) { setValidation({ isValid: false, message: 'Failed to process query', errors: [error instanceof Error ? error.message : 'Unknown error'], }); } finally { setIsLoading(false); } }, []); useEffect(() => { if (query) { const context = getQueryContextAtCursor(query, cursorPos.ch); setQueryContext(context as IQueryContext); } }, [query, cursorPos]); const handleChange = (value: string): void => { setQuery(value); handleQueryChange(value); }; const renderContextBadge = (): JSX.Element | null => { if (!queryContext) return null; let color = 'black'; let text = 'Unknown'; if (queryContext.isInKey) { color = 'blue'; text = 'Key'; } else if (queryContext.isInOperator) { color = 'purple'; text = 'Operator'; } else if (queryContext.isInValue) { color = 'green'; text = 'Value'; } else if (queryContext.isInFunction) { color = 'orange'; text = 'Function'; } else if (queryContext.isInConjunction) { color = 'magenta'; text = 'Conjunction'; } // else if (queryContext.isInParenthesis) { // color = 'grey'; // text = 'Parenthesis'; // } return ( ); }; 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; }[] = []; if (queryContext.isInKey) { options = keySuggestions || []; } else if (queryContext.isInOperator) { options = queryOperatorSuggestions; } else if (queryContext.isInValue) { // refetchQueryKeyValuesSuggestions(); // Fetch values based on the key const key = queryContext.currentToken; // refetchQueryKeyValuesSuggestions({ key }).then((response) => { // if (response && response.data && Array.isArray(response.data.values)) { // options = response.data.values.map((value: string) => ({ // label: value, // type: 'value', // })); // } // }); console.log('key', key, queryContext, query); options = [ { label: 'error', type: 'value' }, { label: 'frontend', type: 'value' }, // Add more value options here ]; } else if (queryContext.isInFunction) { options = [ { label: 'HAS', type: 'function' }, { label: 'HASANY', type: 'function' }, // Add more function options here ]; } else if (queryContext.isInConjunction) { options = [ { label: 'AND', type: 'conjunction' }, { label: 'OR', type: 'conjunction' }, ]; } return { from: word?.from ?? 0, options, }; } return (
Where Clause} extra={ } > Line: {cursorPos.line}, Position: {cursorPos.ch}
Status:
{validation.isValid ? ( <> Valid ) : ( <> Invalid )}
{validation.message && ( )}
{queryContext && (
Token: {queryContext.currentToken || '-'} Type: {queryContext.tokenType || '-'} Context: {renderContextBadge()}
)}
Query Examples
  • status = 'error'
  • service = 'frontend' AND level = 'error'
  • message LIKE '%timeout%'
  • duration {'>'} 1000
  • tags IN ['prod', 'frontend']
  • NOT (status = 'error' OR level = 'error')
); } export default CodeMirrorWhereClause;