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';
|
|
|
|
|
|
2025-04-27 15:34:55 +05:30
|
|
|
import {
|
|
|
|
|
CheckCircleFilled,
|
|
|
|
|
CloseCircleFilled,
|
|
|
|
|
InfoCircleOutlined,
|
|
|
|
|
QuestionCircleOutlined,
|
|
|
|
|
} from '@ant-design/icons';
|
2025-04-27 16:29:35 +05:30
|
|
|
import {
|
|
|
|
|
autocompletion,
|
|
|
|
|
CompletionContext,
|
|
|
|
|
CompletionResult,
|
|
|
|
|
} from '@codemirror/autocomplete';
|
2025-04-27 12:37:02 +05:30
|
|
|
import CodeMirror, { EditorView } from '@uiw/react-codemirror';
|
2025-04-27 15:34:55 +05:30
|
|
|
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 12:37:02 +05:30
|
|
|
import { getQueryContextAtCursor, validateQuery } from 'utils/antlrQueryUtils';
|
|
|
|
|
|
2025-04-27 15:34:55 +05:30
|
|
|
const { Text, Title } = Typography;
|
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({
|
|
|
|
|
// 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);
|
|
|
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-27 15:34:55 +05:30
|
|
|
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 (
|
|
|
|
|
<Badge
|
|
|
|
|
color={color}
|
|
|
|
|
text={text}
|
|
|
|
|
style={{
|
|
|
|
|
color: 'black',
|
|
|
|
|
}}
|
2025-04-27 12:37:02 +05:30
|
|
|
/>
|
2025-04-27 15:34:55 +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;
|
2025-04-27 17:01:19 +05:30
|
|
|
|
|
|
|
|
// 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 || [];
|
2025-04-27 17:01:19 +05:30
|
|
|
} else if (queryContext.isInOperator) {
|
|
|
|
|
options = [
|
2025-04-27 18:23:53 +05:30
|
|
|
{ label: '=', type: 'operator', info: 'Equal to' },
|
|
|
|
|
{ label: '!=', type: 'operator', info: 'Not equal to' },
|
|
|
|
|
{ label: '>', type: 'operator', info: 'Greater than' },
|
|
|
|
|
{ label: '<', type: 'operator', info: 'Less than' },
|
|
|
|
|
{ label: '>=', type: 'operator', info: 'Greater than or equal to' },
|
|
|
|
|
{ label: '<=', type: 'operator', info: 'Less than or equal to' },
|
|
|
|
|
{ label: 'LIKE', type: 'operator', info: 'Like' },
|
|
|
|
|
{ label: 'ILIKE', type: 'operator', info: 'Case insensitive like' },
|
|
|
|
|
{ label: 'BETWEEN', type: 'operator', info: 'Between' },
|
|
|
|
|
{ label: 'EXISTS', type: 'operator', info: 'Exists' },
|
|
|
|
|
{ label: 'REGEXP', type: 'operator', info: 'Regular expression' },
|
|
|
|
|
{ label: 'CONTAINS', type: 'operator', info: 'Contains' },
|
|
|
|
|
{ label: 'IN', type: 'operator', info: 'In' },
|
|
|
|
|
{ label: 'NOT', type: 'operator', info: 'Not' },
|
2025-04-27 17:01:19 +05:30
|
|
|
// Add more operator options here
|
|
|
|
|
];
|
|
|
|
|
} 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);
|
|
|
|
|
|
2025-04-27 17:01:19 +05:30
|
|
|
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' },
|
|
|
|
|
];
|
|
|
|
|
} else if (queryContext.isInParenthesis) {
|
|
|
|
|
options = [
|
|
|
|
|
{ label: '(', type: 'parenthesis' },
|
|
|
|
|
{ label: ')', type: 'parenthesis' },
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-27 16:29:35 +05:30
|
|
|
return {
|
|
|
|
|
from: word?.from ?? 0,
|
2025-04-27 17:01:19 +05:30
|
|
|
options,
|
2025-04-27 16:29:35 +05:30
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-27 15:34:55 +05:30
|
|
|
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="dark"
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
onUpdate={handleUpdate}
|
2025-04-27 16:29:35 +05:30
|
|
|
autoFocus
|
2025-04-27 15:34:55 +05:30
|
|
|
placeholder="Enter your query (e.g., status = 'error' AND service = 'frontend')"
|
2025-04-27 16:29:35 +05:30
|
|
|
extensions={[autocompletion({ override: [myCompletions] })]}
|
2025-04-27 15:34:55 +05:30
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<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 && (
|
2025-04-27 15:34:55 +05:30
|
|
|
<Card size="small" title="Current Context" className="query-context">
|
2025-04-27 12:37:02 +05:30
|
|
|
<div className="context-details">
|
2025-04-27 15:34:55 +05:30
|
|
|
<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>
|
2025-04-27 15:34:55 +05:30
|
|
|
</Card>
|
2025-04-27 12:37:02 +05:30
|
|
|
)}
|
|
|
|
|
|
2025-04-27 15:34:55 +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>
|
2025-04-27 15:34:55 +05:30
|
|
|
<Text code style={{ color: 'black' }}>
|
|
|
|
|
status = 'error'
|
|
|
|
|
</Text>
|
2025-04-27 12:37:02 +05:30
|
|
|
</li>
|
|
|
|
|
<li>
|
2025-04-27 15:34:55 +05:30
|
|
|
<Text code style={{ color: 'black' }}>
|
2025-04-27 12:37:02 +05:30
|
|
|
service = 'frontend' AND level = 'error'
|
|
|
|
|
</Text>
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
2025-04-27 15:34:55 +05:30
|
|
|
<Text code style={{ color: 'black' }}>
|
|
|
|
|
message LIKE '%timeout%'
|
|
|
|
|
</Text>
|
2025-04-27 12:37:02 +05:30
|
|
|
</li>
|
|
|
|
|
<li>
|
2025-04-27 15:34:55 +05:30
|
|
|
<Text code style={{ color: 'black' }}>
|
|
|
|
|
duration {'>'} 1000
|
|
|
|
|
</Text>
|
2025-04-27 12:37:02 +05:30
|
|
|
</li>
|
|
|
|
|
<li>
|
2025-04-27 15:34:55 +05:30
|
|
|
<Text code style={{ color: 'black' }}>
|
|
|
|
|
tags IN ['prod', 'frontend']
|
|
|
|
|
</Text>
|
2025-04-27 12:37:02 +05:30
|
|
|
</li>
|
|
|
|
|
<li>
|
2025-04-27 15:34:55 +05:30
|
|
|
<Text code style={{ color: 'black' }}>
|
2025-04-27 12:37:02 +05:30
|
|
|
NOT (status = 'error' OR level = 'error')
|
|
|
|
|
</Text>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
2025-04-27 15:34:55 +05:30
|
|
|
</Card>
|
2025-04-27 12:37:02 +05:30
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default CodeMirrorWhereClause;
|