every
diff --git a/frontend/src/components/QueryBuilderV2/QueryAggregation/QueryAggregationSelect.tsx b/frontend/src/components/QueryBuilderV2/QueryAggregation/QueryAggregationSelect.tsx
new file mode 100644
index 000000000000..cbf2b70ab9ef
--- /dev/null
+++ b/frontend/src/components/QueryBuilderV2/QueryAggregation/QueryAggregationSelect.tsx
@@ -0,0 +1,210 @@
+/* eslint-disable sonarjs/cognitive-complexity */
+import './QueryAggregation.styles.scss';
+
+import {
+ autocompletion,
+ Completion,
+ CompletionContext,
+} from '@codemirror/autocomplete';
+import { javascript } from '@codemirror/lang-javascript';
+import { copilot } from '@uiw/codemirror-theme-copilot';
+import CodeMirror, { EditorView } from '@uiw/react-codemirror';
+import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
+import { TracesAggregatorOperator } from 'types/common/queryBuilder';
+
+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 },
+};
+
+const fieldSuggestions: Completion[] = [
+ { label: 'duration', type: 'variable' },
+ { label: 'status_code', type: 'variable' },
+ { label: 'service_name', type: 'variable' },
+ { label: 'trace_id', type: 'variable' },
+];
+
+const mapToFunctionCompletions = (
+ operators: typeof tracesAggregateOperatorOptions,
+): Completion[] =>
+ operators.map((op) => ({
+ label: `${op.value}()`,
+ type: 'function',
+ apply: `${op.value}()`,
+ }));
+
+const applyFieldSuggestion = (
+ view: EditorView,
+ suggestion: Completion,
+): void => {
+ const currentText = view.state.sliceDoc(0, view.state.selection.main.from);
+ const endPos = view.state.selection.main.from;
+
+ // Find the last opening parenthesis before the cursor
+ const lastOpenParen = currentText.lastIndexOf('(');
+ if (lastOpenParen === -1) return;
+
+ // Find the last comma after the opening parenthesis
+ const textAfterParen = currentText.slice(lastOpenParen);
+ const lastComma = textAfterParen.lastIndexOf(',');
+
+ // Calculate the start position for insertion
+ const startPos =
+ lastComma === -1 ? lastOpenParen + 1 : lastOpenParen + lastComma + 1;
+
+ // Insert the suggestion
+ view.dispatch({
+ changes: { from: startPos, to: endPos, insert: suggestion.label },
+ selection: { anchor: startPos + suggestion.label.length },
+ });
+};
+
+const applyOperatorSuggestion = (
+ view: EditorView,
+ from: number,
+ label: string,
+): void => {
+ view.dispatch({
+ changes: { from, insert: label },
+ selection: { anchor: from + label.length },
+ });
+};
+
+const getOperatorSuggestions = (
+ from: number,
+ operators: typeof tracesAggregateOperatorOptions,
+): Completion[] =>
+ mapToFunctionCompletions(operators).map((op) => ({
+ ...op,
+ apply: (view: EditorView): void =>
+ applyOperatorSuggestion(view, from, op.label),
+ }));
+
+const aggregatorAutocomplete = autocompletion({
+ override: [
+ (context: CompletionContext): any => {
+ const word = context.matchBefore(/[\w\d_\s]*(\()?[^)]*$/);
+ if (!word || (word.from === word.to && !context.explicit)) return null;
+
+ const textBeforeCursor = context.state.sliceDoc(0, context.pos);
+ const functionMatch = textBeforeCursor.match(/(\w+)\(([^)]*)$/);
+ const funcName = functionMatch?.[1]?.toLowerCase();
+
+ // Handle argument suggestions when cursor is inside parentheses
+ if (funcName && operatorArgMeta[funcName]) {
+ const { acceptsArgs, multiple } = operatorArgMeta[funcName];
+
+ if (!acceptsArgs) return null;
+
+ // Get all arguments for the current function
+ const argsMatch = functionMatch?.[2];
+ const argsSoFar =
+ argsMatch
+ ?.split(',')
+ .map((arg) => arg.trim())
+ .filter(Boolean) || [];
+
+ if (!multiple && argsSoFar.length >= 1) return null;
+
+ return {
+ from: context.pos,
+ options: fieldSuggestions.map((suggestion) => ({
+ ...suggestion,
+ apply: (view: EditorView): void => {
+ applyFieldSuggestion(view, suggestion);
+ // For count_distinct, add a comma after the field
+ if (funcName === TracesAggregatorOperator.COUNT_DISTINCT.toLowerCase()) {
+ const currentPos = view.state.selection.main.from;
+ view.dispatch({
+ changes: { from: currentPos, insert: ', ' },
+ selection: { anchor: currentPos + 2 },
+ });
+ }
+ },
+ })),
+ };
+ }
+
+ // Handle operator suggestions
+ const isAfterCompleteFunction = textBeforeCursor.match(/\w+\([^)]*\)\s*$/);
+ if (isAfterCompleteFunction) {
+ return {
+ from: context.pos,
+ options: getOperatorSuggestions(
+ context.pos,
+ tracesAggregateOperatorOptions,
+ ),
+ };
+ }
+
+ // Regular word-based suggestions
+ const wordBeforeCursor = word.text.trim();
+ if (wordBeforeCursor) {
+ const filteredOperators = tracesAggregateOperatorOptions.filter((op) =>
+ op.value.toLowerCase().startsWith(wordBeforeCursor.toLowerCase()),
+ );
+ return {
+ from: word.from,
+ options: getOperatorSuggestions(word.from, filteredOperators),
+ };
+ }
+
+ // Show all options if no word before cursor
+ return {
+ from: word.from,
+ options: getOperatorSuggestions(word.from, tracesAggregateOperatorOptions),
+ };
+ },
+ ],
+});
+
+function QueryAggregationSelect(): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+export default QueryAggregationSelect;
diff --git a/frontend/src/components/QueryBuilderV2/QueryAggregationOptions/QueryAggregationOptions.styles.scss b/frontend/src/components/QueryBuilderV2/QueryAggregationOptions/QueryAggregationOptions.styles.scss
deleted file mode 100644
index 9747aa472300..000000000000
--- a/frontend/src/components/QueryBuilderV2/QueryAggregationOptions/QueryAggregationOptions.styles.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-.query-aggregation-container {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 8px;
-
- .query-aggregation-options-input {
- width: 100%;
- height: 36px;
- line-height: 36px;
- border-radius: 2px;
- border: 1px solid var(--bg-slate-400);
- box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
-
- font-family: 'Space Mono', monospace !important;
-
- &::placeholder {
- color: var(--bg-vanilla-100);
- opacity: 0.5;
- }
- }
-
- .query-aggregation-interval {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 8px;
- }
-}
diff --git a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx
index b0dc3ced69a7..746cb87e4585 100644
--- a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx
+++ b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.tsx
@@ -3,7 +3,7 @@ import './QueryBuilderV2.styles.scss';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import QueryAddOns from './QueryAddOns/QueryAddOns';
-import QueryAggregationOptions from './QueryAggregationOptions/QueryAggregationOptions';
+import QueryAggregation from './QueryAggregation/QueryAggregation';
import QuerySearch from './QuerySearch/QuerySearch';
function QueryBuilderV2(): JSX.Element {
@@ -12,7 +12,7 @@ function QueryBuilderV2(): JSX.Element {
return (
-
+
0) parens--;
+ state = State.ExpectOperator;
+ break;
+ case FilterQueryLexer.LBRACK:
+ array++;
+ state = State.ExpectValue;
+ break;
+ case FilterQueryLexer.RBRACK:
+ if (array > 0) array--;
+ state = State.ExpectOperator;
+ break;
+ case FilterQueryLexer.COMMA:
+ if (array > 0) state = State.ExpectValue;
+ break;
+ case FilterQueryLexer.KEY:
+ if (state === State.ExpectKey) {
+ lastKey = tok;
+ state = State.ExpectOperator;
+ }
+ break;
+ case FilterQueryLexer.QUOTED_TEXT:
+ case FilterQueryLexer.NUMBER:
+ case FilterQueryLexer.BOOL:
+ if (state === State.ExpectValue) {
+ state = State.ExpectOperator;
+ }
+ break;
+ default:
+ if (
+ tok.type >= FilterQueryLexer.EQUALS &&
+ tok.type <= FilterQueryLexer.CONTAINS
+ ) {
+ state = State.ExpectValue;
+ }
+ break;
+ }
+
+ pos += text.length;
+ }
+
+ console.log('cursorTok', cursorTok);
+
+ const out: ContextInfo = { context: CursorContext.NoFilter };
+
+ if (cursorTok) {
+ out.token = cursorTok;
+ }
+
+ console.log('out', cloneDeep(out));
+ console.log('state', cloneDeep(state));
+
+ switch (state) {
+ case State.ExpectKey:
+ out.context = CursorContext.Key;
+ break;
+ case State.ExpectOperator:
+ out.context = CursorContext.Operator;
+ if (lastKey) out.key = lastKey.text;
+ break;
+ case State.ExpectValue:
+ out.context = CursorContext.Value;
+ if (lastKey) out.key = lastKey.text;
+
+ if (lastOperator) {
+ out.operator = lastOperator.text;
+ }
+ break;
+ default:
+ out.context = CursorContext.NoFilter;
+ break;
+ }
+
+ console.log('out', cloneDeep(out));
+
+ if (
+ cursorTok &&
+ cursorTok.type === FilterQueryLexer.QUOTED_TEXT &&
+ (out.context === CursorContext.Key || out.context === CursorContext.NoFilter)
+ ) {
+ out.context = CursorContext.FullText;
+ }
+
+ // if (!cursorTok || cursorTok.type === antlr4.Token.EOF) {
+ // out.context = CursorContext.NoFilter;
+ // }
+
+ return out;
+}