466 lines
12 KiB
TypeScript
Raw Normal View History

2025-04-27 16:29:35 +05:30
/* eslint-disable import/no-extraneous-dependencies */
2025-04-27 12:37:02 +05:30
/* eslint-disable no-nested-ternary */
import './CodeMirrorWhereClause.styles.scss';
import {
CheckCircleFilled,
CloseCircleFilled,
InfoCircleOutlined,
QuestionCircleOutlined,
} from '@ant-design/icons';
2025-04-27 16:29:35 +05:30
import {
autocompletion,
CompletionContext,
CompletionResult,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { copilot } from '@uiw/codemirror-theme-copilot';
2025-04-27 19:34:07 +05:30
import CodeMirror, { EditorView, Extension } from '@uiw/react-codemirror';
import { Badge, Card, Divider, Space, Tooltip, Typography } from 'antd';
2025-04-27 18:23:53 +05:30
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
// import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
2025-04-27 12:37:02 +05:30
import { useCallback, useEffect, useRef, useState } from 'react';
import { IQueryContext, IValidationResult } from 'types/antlrQueryTypes';
2025-04-27 18:23:53 +05:30
import { QueryKeySuggestionsProps } from 'types/api/querySuggestions/types';
2025-04-27 19:34:07 +05:30
import {
getQueryContextAtCursor,
queryOperatorSuggestions,
validateQuery,
} from 'utils/antlrQueryUtils';
2025-04-27 12:37:02 +05:30
const { Text, Title } = Typography;
2025-04-27 12:37:02 +05:30
2025-04-27 19:34:07 +05:30
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;
});
}
2025-04-27 12:37:02 +05:30
function CodeMirrorWhereClause(): JSX.Element {
const [query, setQuery] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [queryContext, setQueryContext] = useState<IQueryContext | null>(null);
const [validation, setValidation] = useState<IValidationResult>({
isValid: false,
message: '',
errors: [],
});
2025-04-27 18:23:53 +05:30
const [keySuggestions, setKeySuggestions] = useState<
QueryKeySuggestionsProps[] | null
>(null);
2025-04-27 12:37:02 +05:30
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const lastPosRef = useRef<{ line: number; ch: number }>({ line: 0, ch: 0 });
2025-04-27 18:23:53 +05:30
const {
data: queryKeySuggestions,
// isLoading: queryKeySuggestionsLoading,
// isRefetching: queryKeySuggestionsRefetching,
// refetch: queryKeySuggestionsRefetch,
// error: queryKeySuggestionsError,
// isError: queryKeySuggestionsIsError,
} = useGetQueryKeySuggestions({ signal: 'traces' });
// const {
// data: queryKeyValuesSuggestions,
// isLoading: queryKeyValuesSuggestionsLoading,
// refetch: refetchQueryKeyValuesSuggestions,
// } = useGetQueryKeyValueSuggestions({
// key: 'status',
2025-04-27 20:37:42 +05:30
// signal: 'traces',
2025-04-27 18:23:53 +05:30
// });
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);
2025-04-27 12:37:02 +05:30
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';
}
2025-04-27 19:34:07 +05:30
// else if (queryContext.isInParenthesis) {
// color = 'grey';
// text = 'Parenthesis';
// }
return (
<Badge
color={color}
text={text}
style={{
color: 'black',
}}
2025-04-27 12:37:02 +05:30
/>
);
};
2025-04-27 12:37:02 +05:30
2025-04-27 16:29:35 +05:30
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) {
2025-04-27 18:23:53 +05:30
options = keySuggestions || [];
} else if (queryContext.isInOperator) {
2025-04-27 19:34:07 +05:30
options = queryOperatorSuggestions;
} else if (queryContext.isInValue) {
2025-04-27 18:23:53 +05:30
// 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' },
];
}
2025-04-27 16:29:35 +05:30
return {
from: word?.from ?? 0,
options,
2025-04-27 16:29:35 +05:30
};
}
2025-04-27 20:37:42 +05:30
const customTheme = EditorView.theme({
'&': {
fontFamily: '"Space Mono", monospace', // Change to any font
fontSize: '13px', // Set font size
lineHeight: '1.8', // Set line height
margin: '8px 0px',
},
'.cm-line': {
lineHeight: '2.2', // Set line height
},
'.cm-gutters': {
lineHeight: '1.8', // Set line height
display: 'none',
},
'.cm-content': {
lineHeight: '2.2', // Set line height
borderRadius: '2px',
border: '1px solid var(--bg-ink-400) !important',
background: 'var(--bg-ink-400) !important',
padding: '0px',
},
'.cm-focused': {
border: '1px solid var(--bg-robin-500) !important',
background: 'var(--bg-ink-400) !important',
},
});
return (
<div className="code-mirror-where-clause">
<Card
size="small"
title={<Title level={5}>Where Clause</Title>}
extra={
<Tooltip title="Write a query to filter your data">
<QuestionCircleOutlined />
</Tooltip>
}
>
<CodeMirror
value={query}
theme={copilot}
onChange={handleChange}
onUpdate={handleUpdate}
2025-04-27 16:29:35 +05:30
autoFocus
placeholder="Enter your query (e.g., status = 'error' AND service = 'frontend')"
2025-04-27 19:34:07 +05:30
extensions={[
autocompletion({ override: [myCompletions] }),
collapseSpacesOutsideStrings(),
javascript({ jsx: false, typescript: false }),
2025-04-27 20:37:42 +05:30
customTheme,
2025-04-27 19:34:07 +05:30
]}
2025-04-27 20:37:42 +05:30
basicSetup={{
lineNumbers: false,
}}
/>
<Space className="cursor-position" size={4}>
<InfoCircleOutlined />
<Text style={{ color: 'black' }}>
Line: {cursorPos.line}, Position: {cursorPos.ch}
</Text>
</Space>
<Divider style={{ margin: '8px 0' }} />
<div className="query-validation">
<Text>Status:</Text>
<div className={validation.isValid ? 'valid' : 'invalid'}>
{validation.isValid ? (
<>
<CheckCircleFilled /> Valid
</>
) : (
<>
<CloseCircleFilled /> Invalid
</>
)}
</div>
{validation.message && (
<Tooltip title={validation.message}>
<InfoCircleOutlined style={{ marginLeft: 8 }} />
</Tooltip>
)}
</div>
</Card>
2025-04-27 12:37:02 +05:30
{queryContext && (
<Card size="small" title="Current Context" className="query-context">
2025-04-27 12:37:02 +05:30
<div className="context-details">
<Space direction="vertical" size={4}>
<Space>
<Text strong style={{ color: 'black' }}>
Token:
</Text>
<Text code style={{ color: 'black' }}>
{queryContext.currentToken || '-'}
</Text>
</Space>
<Space>
<Text strong style={{ color: 'black' }}>
Type:
</Text>
<Text style={{ color: 'black' }}>{queryContext.tokenType || '-'}</Text>
</Space>
<Space>
<Text strong style={{ color: 'black' }}>
Context:
</Text>
{renderContextBadge()}
</Space>
</Space>
2025-04-27 12:37:02 +05:30
</div>
</Card>
2025-04-27 12:37:02 +05:30
)}
<Card
size="small"
title="Query Examples"
className="query-examples"
style={{
backgroundColor: 'var(--bg-vanilla-100)',
color: 'black',
}}
>
<div className="query-examples-list">Query Examples</div>
2025-04-27 12:37:02 +05:30
<ul>
<li>
<Text code style={{ color: 'black' }}>
status = &apos;error&apos;
</Text>
2025-04-27 12:37:02 +05:30
</li>
<li>
<Text code style={{ color: 'black' }}>
2025-04-27 12:37:02 +05:30
service = &apos;frontend&apos; AND level = &apos;error&apos;
</Text>
</li>
<li>
<Text code style={{ color: 'black' }}>
message LIKE &apos;%timeout%&apos;
</Text>
2025-04-27 12:37:02 +05:30
</li>
<li>
<Text code style={{ color: 'black' }}>
duration {'>'} 1000
</Text>
2025-04-27 12:37:02 +05:30
</li>
<li>
<Text code style={{ color: 'black' }}>
tags IN [&apos;prod&apos;, &apos;frontend&apos;]
</Text>
2025-04-27 12:37:02 +05:30
</li>
<li>
<Text code style={{ color: 'black' }}>
2025-04-27 12:37:02 +05:30
NOT (status = &apos;error&apos; OR level = &apos;error&apos;)
</Text>
</li>
</ul>
</Card>
2025-04-27 12:37:02 +05:30
</div>
);
}
export default CodeMirrorWhereClause;