2025-05-14 01:56:25 +05:30
|
|
|
/* eslint-disable import/no-extraneous-dependencies */
|
|
|
|
|
/* eslint-disable no-cond-assign */
|
|
|
|
|
/* eslint-disable no-restricted-syntax */
|
|
|
|
|
/* eslint-disable class-methods-use-this */
|
|
|
|
|
/* eslint-disable react/no-this-in-sfc */
|
2025-05-13 16:31:37 +05:30
|
|
|
/* eslint-disable sonarjs/cognitive-complexity */
|
|
|
|
|
import './QueryAggregation.styles.scss';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
autocompletion,
|
2025-05-14 13:16:49 +05:30
|
|
|
closeCompletion,
|
2025-05-13 16:31:37 +05:30
|
|
|
Completion,
|
|
|
|
|
CompletionContext,
|
2025-05-14 13:16:49 +05:30
|
|
|
completionKeymap,
|
2025-05-13 23:04:20 +05:30
|
|
|
CompletionResult,
|
2025-05-13 16:31:37 +05:30
|
|
|
} from '@codemirror/autocomplete';
|
|
|
|
|
import { javascript } from '@codemirror/lang-javascript';
|
2025-05-14 01:56:25 +05:30
|
|
|
import { RangeSetBuilder } from '@codemirror/state';
|
2025-05-13 16:31:37 +05:30
|
|
|
import { copilot } from '@uiw/codemirror-theme-copilot';
|
2025-05-14 01:56:25 +05:30
|
|
|
import CodeMirror, {
|
|
|
|
|
Decoration,
|
|
|
|
|
EditorView,
|
2025-05-14 13:16:49 +05:30
|
|
|
keymap,
|
2025-05-14 01:56:25 +05:30
|
|
|
ViewPlugin,
|
|
|
|
|
ViewUpdate,
|
|
|
|
|
} from '@uiw/react-codemirror';
|
2025-05-13 23:04:20 +05:30
|
|
|
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
|
|
|
|
|
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
2025-05-13 16:31:37 +05:30
|
|
|
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
|
2025-05-13 23:04:20 +05:30
|
|
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
2025-05-14 13:58:56 +05:30
|
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
2025-05-13 23:04:20 +05:30
|
|
|
import { useQuery } from 'react-query';
|
|
|
|
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
2025-05-13 16:31:37 +05:30
|
|
|
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
|
|
|
|
|
|
2025-05-14 01:56:25 +05:30
|
|
|
const chipDecoration = Decoration.mark({
|
|
|
|
|
class: 'chip-decorator',
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-13 16:31:37 +05:30
|
|
|
const operatorArgMeta: Record<
|
|
|
|
|
string,
|
|
|
|
|
{ acceptsArgs: boolean; multiple: boolean }
|
|
|
|
|
> = {
|
|
|
|
|
[TracesAggregatorOperator.NOOP]: { acceptsArgs: false, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.COUNT]: { acceptsArgs: false, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.COUNT_DISTINCT]: {
|
|
|
|
|
acceptsArgs: true,
|
|
|
|
|
multiple: true,
|
|
|
|
|
},
|
|
|
|
|
[TracesAggregatorOperator.SUM]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.AVG]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.MAX]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.MIN]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.P05]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.P10]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.P20]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.P25]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.P50]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.P75]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.P90]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.P95]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.P99]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.RATE]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.RATE_SUM]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.RATE_AVG]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.RATE_MIN]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
[TracesAggregatorOperator.RATE_MAX]: { acceptsArgs: true, multiple: false },
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-13 23:04:20 +05:30
|
|
|
function getFunctionContextAtCursor(
|
|
|
|
|
text: string,
|
|
|
|
|
cursorPos: number,
|
|
|
|
|
): string | null {
|
|
|
|
|
// Find the nearest function name to the left of the nearest unmatched '('
|
|
|
|
|
let openParenIndex = -1;
|
|
|
|
|
let funcName: string | null = null;
|
|
|
|
|
let parenStack = 0;
|
|
|
|
|
for (let i = cursorPos - 1; i >= 0; i--) {
|
|
|
|
|
if (text[i] === ')') parenStack++;
|
|
|
|
|
else if (text[i] === '(') {
|
|
|
|
|
if (parenStack === 0) {
|
|
|
|
|
openParenIndex = i;
|
|
|
|
|
const before = text.slice(0, i);
|
|
|
|
|
const match = before.match(/(\w+)\s*$/);
|
|
|
|
|
if (match) funcName = match[1].toLowerCase();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
parenStack--;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (openParenIndex === -1 || !funcName) return null;
|
|
|
|
|
// Scan forwards to find the matching closing parenthesis
|
|
|
|
|
let closeParenIndex = -1;
|
|
|
|
|
let depth = 1;
|
|
|
|
|
for (let j = openParenIndex + 1; j < text.length; j++) {
|
|
|
|
|
if (text[j] === '(') depth++;
|
|
|
|
|
else if (text[j] === ')') depth--;
|
|
|
|
|
if (depth === 0) {
|
|
|
|
|
closeParenIndex = j;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
cursorPos > openParenIndex &&
|
|
|
|
|
(closeParenIndex === -1 || cursorPos <= closeParenIndex)
|
|
|
|
|
) {
|
|
|
|
|
return funcName;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-05-13 16:31:37 +05:30
|
|
|
|
2025-05-14 01:56:25 +05:30
|
|
|
// eslint-disable-next-line react/no-this-in-sfc
|
2025-05-14 16:16:34 +05:30
|
|
|
function QueryAggregationSelect({
|
|
|
|
|
onAggregationOptionsSelect,
|
|
|
|
|
}: {
|
|
|
|
|
onAggregationOptionsSelect: (value: { func: string; arg: string }[]) => void;
|
|
|
|
|
}): JSX.Element {
|
2025-05-13 23:04:20 +05:30
|
|
|
const { currentQuery } = useQueryBuilder();
|
|
|
|
|
const queryData = currentQuery.builder.queryData[0];
|
|
|
|
|
const [input, setInput] = useState('');
|
|
|
|
|
const [cursorPos, setCursorPos] = useState(0);
|
2025-05-14 13:58:56 +05:30
|
|
|
const [functionArgPairs, setFunctionArgPairs] = useState<
|
|
|
|
|
{ func: string; arg: string }[]
|
|
|
|
|
>([]);
|
2025-05-13 23:04:20 +05:30
|
|
|
const editorRef = useRef<EditorView | null>(null);
|
|
|
|
|
|
|
|
|
|
// Update cursor position on every editor update
|
|
|
|
|
const handleUpdate = (update: { view: EditorView }): void => {
|
|
|
|
|
const pos = update.view.state.selection.main.from;
|
|
|
|
|
setCursorPos(pos);
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-14 13:58:56 +05:30
|
|
|
// Extract all valid function-argument pairs from the input
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const pairs: { func: string; arg: string }[] = [];
|
|
|
|
|
const regex = /([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
|
|
|
|
|
let match;
|
|
|
|
|
while ((match = regex.exec(input)) !== null) {
|
|
|
|
|
const func = match[1].toLowerCase();
|
|
|
|
|
const args = match[2]
|
|
|
|
|
.split(',')
|
|
|
|
|
.map((arg) => arg.trim())
|
|
|
|
|
.filter((arg) => arg.length > 0);
|
|
|
|
|
args.forEach((arg) => {
|
|
|
|
|
pairs.push({ func, arg });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
setFunctionArgPairs(pairs);
|
2025-05-14 16:16:34 +05:30
|
|
|
onAggregationOptionsSelect(pairs);
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2025-05-14 13:58:56 +05:30
|
|
|
}, [input]);
|
|
|
|
|
|
2025-05-13 23:04:20 +05:30
|
|
|
// Find function context for fetching suggestions
|
|
|
|
|
const functionContextForFetch = getFunctionContextAtCursor(input, cursorPos);
|
|
|
|
|
|
|
|
|
|
const { data: aggregateAttributeData, isLoading: isLoadingFields } = useQuery(
|
|
|
|
|
[
|
|
|
|
|
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
|
|
|
|
functionContextForFetch,
|
|
|
|
|
queryData.dataSource,
|
|
|
|
|
],
|
|
|
|
|
() =>
|
|
|
|
|
getAggregateAttribute({
|
|
|
|
|
searchText: '',
|
|
|
|
|
aggregateOperator: functionContextForFetch as string,
|
|
|
|
|
dataSource: queryData.dataSource,
|
|
|
|
|
}),
|
|
|
|
|
{
|
|
|
|
|
enabled:
|
|
|
|
|
!!functionContextForFetch &&
|
|
|
|
|
!!operatorArgMeta[functionContextForFetch]?.acceptsArgs,
|
|
|
|
|
},
|
|
|
|
|
);
|
2025-05-13 16:31:37 +05:30
|
|
|
|
2025-05-14 01:56:25 +05:30
|
|
|
// Get valid function names (lowercase)
|
|
|
|
|
const validFunctions = useMemo(
|
|
|
|
|
() => tracesAggregateOperatorOptions.map((op) => op.value.toLowerCase()),
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Memoized chipPlugin that highlights valid function calls like count(), max(arg), min(arg)
|
|
|
|
|
const chipPlugin = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
ViewPlugin.fromClass(
|
|
|
|
|
class {
|
|
|
|
|
decorations: import('@codemirror/view').DecorationSet;
|
|
|
|
|
|
|
|
|
|
constructor(view: EditorView) {
|
|
|
|
|
this.decorations = this.buildDecorations(view);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update(update: ViewUpdate): void {
|
|
|
|
|
if (update.docChanged || update.viewportChanged) {
|
|
|
|
|
this.decorations = this.buildDecorations(update.view);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buildDecorations(
|
|
|
|
|
view: EditorView,
|
|
|
|
|
): import('@codemirror/view').DecorationSet {
|
|
|
|
|
const builder = new RangeSetBuilder<Decoration>();
|
|
|
|
|
for (const { from, to } of view.visibleRanges) {
|
|
|
|
|
const text = view.state.doc.sliceString(from, to);
|
|
|
|
|
|
|
|
|
|
const regex = /\b([a-zA-Z_][\w]*)\s*\(([^)]*)\)/g;
|
|
|
|
|
let match;
|
|
|
|
|
|
|
|
|
|
while ((match = regex.exec(text)) !== null) {
|
|
|
|
|
const func = match[1].toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (validFunctions.includes(func)) {
|
|
|
|
|
const start = from + match.index;
|
|
|
|
|
const end = start + match[0].length;
|
|
|
|
|
builder.add(start, end, chipDecoration);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return builder.finish();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
decorations: (v: any): import('@codemirror/view').DecorationSet =>
|
|
|
|
|
v.decorations,
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
[validFunctions],
|
|
|
|
|
) as any;
|
|
|
|
|
|
2025-05-13 23:04:20 +05:30
|
|
|
const operatorCompletions: Completion[] = tracesAggregateOperatorOptions.map(
|
|
|
|
|
(op) => ({
|
|
|
|
|
label: op.value,
|
|
|
|
|
type: 'function',
|
|
|
|
|
info: op.label,
|
2025-05-14 01:56:25 +05:30
|
|
|
apply: (
|
|
|
|
|
view: EditorView,
|
|
|
|
|
completion: Completion,
|
|
|
|
|
from: number,
|
|
|
|
|
to: number,
|
|
|
|
|
): void => {
|
2025-05-14 13:11:06 +05:30
|
|
|
const isCount = op.value === TracesAggregatorOperator.COUNT;
|
|
|
|
|
const insertText = isCount ? `${op.value}() ` : `${op.value}(`;
|
|
|
|
|
const cursorPos = isCount
|
|
|
|
|
? from + op.value.length + 3 // after 'count() '
|
|
|
|
|
: from + op.value.length + 1; // after 'operator('
|
2025-05-13 23:04:20 +05:30
|
|
|
view.dispatch({
|
2025-05-14 01:56:25 +05:30
|
|
|
changes: { from, to, insert: insertText },
|
2025-05-13 23:04:20 +05:30
|
|
|
selection: { anchor: cursorPos },
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
2025-05-13 16:31:37 +05:30
|
|
|
|
2025-05-14 13:11:06 +05:30
|
|
|
// Memoize field suggestions from API (no filtering here)
|
2025-05-13 23:04:20 +05:30
|
|
|
const fieldSuggestions = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
aggregateAttributeData?.payload?.attributeKeys?.map(
|
|
|
|
|
(attributeKey: BaseAutocompleteData) => ({
|
|
|
|
|
label: attributeKey.key,
|
|
|
|
|
type: 'variable',
|
|
|
|
|
info: attributeKey.dataType,
|
2025-05-14 01:56:25 +05:30
|
|
|
apply: (
|
|
|
|
|
view: EditorView,
|
|
|
|
|
completion: Completion,
|
|
|
|
|
from: number,
|
|
|
|
|
to: number,
|
|
|
|
|
): void => {
|
2025-05-13 23:04:20 +05:30
|
|
|
view.dispatch({
|
2025-05-14 13:11:06 +05:30
|
|
|
changes: { from, to, insert: completion.label },
|
|
|
|
|
selection: { anchor: from + completion.label.length },
|
2025-05-13 23:04:20 +05:30
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
) || [],
|
|
|
|
|
[aggregateAttributeData],
|
|
|
|
|
);
|
2025-05-13 16:31:37 +05:30
|
|
|
|
2025-05-13 23:04:20 +05:30
|
|
|
const aggregatorAutocomplete = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
autocompletion({
|
|
|
|
|
override: [
|
|
|
|
|
(context: CompletionContext): CompletionResult | null => {
|
|
|
|
|
const text = context.state.sliceDoc(0, context.state.doc.length);
|
|
|
|
|
const cursorPos = context.pos;
|
|
|
|
|
const funcName = getFunctionContextAtCursor(text, cursorPos);
|
|
|
|
|
|
2025-05-14 01:56:25 +05:30
|
|
|
// Do not show suggestions if inside count()
|
|
|
|
|
if (
|
|
|
|
|
funcName === TracesAggregatorOperator.COUNT &&
|
|
|
|
|
cursorPos > 0 &&
|
|
|
|
|
text[cursorPos - 1] !== ')'
|
|
|
|
|
) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-13 23:04:20 +05:30
|
|
|
// If inside a function that accepts args, show field suggestions
|
|
|
|
|
if (funcName && operatorArgMeta[funcName]?.acceptsArgs) {
|
|
|
|
|
if (isLoadingFields) {
|
|
|
|
|
return {
|
|
|
|
|
from: cursorPos,
|
|
|
|
|
options: [
|
|
|
|
|
{
|
|
|
|
|
label: 'Loading suggestions...',
|
|
|
|
|
type: 'text',
|
|
|
|
|
apply: (): void => {},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
2025-05-13 16:31:37 +05:30
|
|
|
}
|
2025-05-14 13:11:06 +05:30
|
|
|
|
|
|
|
|
const doc = context.state.sliceDoc(0, cursorPos);
|
|
|
|
|
const lastOpenParen = doc.lastIndexOf('(');
|
|
|
|
|
const lastComma = doc.lastIndexOf(',', cursorPos - 1);
|
|
|
|
|
const startOfArg =
|
|
|
|
|
lastComma > lastOpenParen ? lastComma + 1 : lastOpenParen + 1;
|
|
|
|
|
const inputText = doc.slice(startOfArg, cursorPos).trim();
|
|
|
|
|
|
|
|
|
|
// Parse arguments already present in the function call (before the cursor)
|
|
|
|
|
const usedArgs = new Set<string>();
|
|
|
|
|
if (lastOpenParen !== -1) {
|
|
|
|
|
const argsString = doc.slice(lastOpenParen + 1, cursorPos);
|
|
|
|
|
argsString.split(',').forEach((arg) => {
|
|
|
|
|
const trimmed = arg.trim();
|
|
|
|
|
if (trimmed) usedArgs.add(trimmed);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-14 13:58:56 +05:30
|
|
|
// Exclude arguments already paired with this function elsewhere in the input
|
|
|
|
|
const globalUsedArgs = new Set(
|
|
|
|
|
functionArgPairs
|
|
|
|
|
.filter((pair) => pair.func === funcName)
|
|
|
|
|
.map((pair) => pair.arg),
|
|
|
|
|
);
|
|
|
|
|
|
2025-05-14 13:11:06 +05:30
|
|
|
const availableSuggestions = fieldSuggestions.filter(
|
2025-05-14 13:58:56 +05:30
|
|
|
(suggestion) =>
|
|
|
|
|
!usedArgs.has(suggestion.label) &&
|
|
|
|
|
!globalUsedArgs.has(suggestion.label),
|
2025-05-14 13:11:06 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const filteredSuggestions =
|
|
|
|
|
inputText === ''
|
|
|
|
|
? availableSuggestions
|
|
|
|
|
: availableSuggestions.filter((suggestion) =>
|
|
|
|
|
suggestion.label.toLowerCase().includes(inputText.toLowerCase()),
|
|
|
|
|
);
|
|
|
|
|
|
2025-05-13 23:04:20 +05:30
|
|
|
return {
|
2025-05-14 13:11:06 +05:30
|
|
|
from: startOfArg,
|
|
|
|
|
options: filteredSuggestions,
|
2025-05-13 23:04:20 +05:30
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-14 13:58:56 +05:30
|
|
|
// Before returning operatorCompletions, filter out 'count' if already present in the input (case-insensitive, direct text check)
|
|
|
|
|
if (!funcName || !operatorArgMeta[funcName]?.acceptsArgs) {
|
|
|
|
|
// Check if 'count(' is present in the current input (case-insensitive)
|
|
|
|
|
const hasCount = text.toLowerCase().includes('count(');
|
|
|
|
|
const availableOperators = hasCount
|
|
|
|
|
? operatorCompletions.filter((op) => op.label.toLowerCase() !== 'count')
|
|
|
|
|
: operatorCompletions;
|
|
|
|
|
const word = context.matchBefore(/[\w\d_]+/);
|
|
|
|
|
if (!word && !context.explicit) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
from: word ? word.from : context.pos,
|
|
|
|
|
options: availableOperators,
|
|
|
|
|
};
|
2025-05-14 13:16:49 +05:30
|
|
|
}
|
2025-05-14 13:58:56 +05:30
|
|
|
|
|
|
|
|
return null;
|
2025-05-13 23:04:20 +05:30
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
defaultKeymap: true,
|
|
|
|
|
closeOnBlur: false,
|
|
|
|
|
maxRenderedOptions: 50,
|
|
|
|
|
activateOnTyping: true,
|
|
|
|
|
}),
|
2025-05-14 13:58:56 +05:30
|
|
|
[operatorCompletions, isLoadingFields, fieldSuggestions, functionArgPairs],
|
2025-05-13 23:04:20 +05:30
|
|
|
);
|
2025-05-13 16:31:37 +05:30
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="query-aggregation-select-container">
|
|
|
|
|
<CodeMirror
|
2025-05-13 23:04:20 +05:30
|
|
|
value={input}
|
|
|
|
|
onChange={setInput}
|
2025-05-13 16:31:37 +05:30
|
|
|
className="query-aggregation-select-editor"
|
|
|
|
|
width="100%"
|
|
|
|
|
theme={copilot}
|
|
|
|
|
extensions={[
|
2025-05-14 01:56:25 +05:30
|
|
|
chipPlugin,
|
2025-05-13 16:31:37 +05:30
|
|
|
aggregatorAutocomplete,
|
2025-05-13 23:04:20 +05:30
|
|
|
javascript({ jsx: false, typescript: false }),
|
2025-05-14 13:16:49 +05:30
|
|
|
keymap.of([
|
|
|
|
|
...completionKeymap,
|
|
|
|
|
{
|
|
|
|
|
key: 'Escape',
|
|
|
|
|
run: closeCompletion,
|
|
|
|
|
},
|
|
|
|
|
]),
|
2025-05-13 16:31:37 +05:30
|
|
|
]}
|
|
|
|
|
placeholder="Type aggregator functions like sum(), count_distinct(...), etc."
|
|
|
|
|
basicSetup={{
|
|
|
|
|
lineNumbers: false,
|
|
|
|
|
autocompletion: true,
|
|
|
|
|
completionKeymap: true,
|
|
|
|
|
}}
|
2025-05-13 23:04:20 +05:30
|
|
|
onUpdate={handleUpdate}
|
|
|
|
|
ref={editorRef}
|
2025-05-13 16:31:37 +05:30
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default QueryAggregationSelect;
|