diff --git a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.styles.scss b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.styles.scss
index 03b4a57a6c4a..c8b52b5fcb84 100644
--- a/frontend/src/components/QueryBuilderV2/QueryBuilderV2.styles.scss
+++ b/frontend/src/components/QueryBuilderV2/QueryBuilderV2.styles.scss
@@ -17,6 +17,7 @@
.qb-content-container {
display: flex;
flex-direction: column;
+ width: calc(100% - 44px);
flex: 1;
diff --git a/frontend/src/components/QueryBuilderV2/QueryV2/QueryAddOns/HavingFilter/HavingFilter.tsx b/frontend/src/components/QueryBuilderV2/QueryV2/QueryAddOns/HavingFilter/HavingFilter.tsx
index 689e5884c35e..de2769fea1a4 100644
--- a/frontend/src/components/QueryBuilderV2/QueryV2/QueryAddOns/HavingFilter/HavingFilter.tsx
+++ b/frontend/src/components/QueryBuilderV2/QueryV2/QueryAddOns/HavingFilter/HavingFilter.tsx
@@ -121,10 +121,10 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
const isAfterOperator = (tokens: string[]): boolean => {
if (tokens.length === 0) return false;
const lastToken = tokens[tokens.length - 1];
- // Check if the last token ends with any operator (with or without space)
+ // Check if the last token is exactly an operator or ends with an operator and space
return havingOperators.some((op) => {
const opWithSpace = `${op.value} `;
- return lastToken.endsWith(op.value) || lastToken.endsWith(opWithSpace);
+ return lastToken === op.value || lastToken.endsWith(opWithSpace);
});
};
@@ -152,6 +152,21 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
};
}
+ // Show value suggestions after operator - this should take precedence
+ if (isAfterOperator(tokens)) {
+ return {
+ from: context.pos,
+ options: [
+ ...commonValues,
+ {
+ label: 'Enter a custom number value',
+ type: 'text',
+ apply: (): boolean => true,
+ },
+ ],
+ };
+ }
+
// Suggest key/operator pairs and ( for grouping
if (
tokens.length === 0 ||
@@ -164,21 +179,18 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
};
}
- // Show value suggestions after operator
- if (isAfterOperator(tokens)) {
- return {
- from: context.pos,
- options: [
- ...commonValues,
- {
- label: 'Enter a custom number value',
- type: 'text',
- apply: (): boolean =>
- // Don't insert any text, just let the user type
- true,
- },
- ],
- };
+ // Show suggestions when typing
+ if (tokens.length > 0) {
+ const lastToken = tokens[tokens.length - 1];
+ const filteredOptions = options.filter((opt) =>
+ opt.label.toLowerCase().includes(lastToken.toLowerCase()),
+ );
+ if (filteredOptions.length > 0) {
+ return {
+ from: context.pos - lastToken.length,
+ options: filteredOptions,
+ };
+ }
}
// Suggest ) for grouping after a value and a space, if there are unmatched (
@@ -205,12 +217,16 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
};
}
- return null;
+ // Show all options if no other condition matches
+ return {
+ from: context.pos,
+ options,
+ };
},
],
defaultKeymap: true,
closeOnBlur: false,
- maxRenderedOptions: 50,
+ maxRenderedOptions: 200,
activateOnTyping: true,
}),
[options],
@@ -218,11 +234,11 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
return (
-
+
-
+
+
- {showAggregationInterval && (
-
-
every
-
-
{}}
- />
+ {showAggregationInterval && (
+
-
- )}
+ )}
+
);
}
diff --git a/frontend/src/components/QueryBuilderV2/QueryV2/QueryAggregation/QueryAggregationSelect.tsx b/frontend/src/components/QueryBuilderV2/QueryV2/QueryAggregation/QueryAggregationSelect.tsx
index 3d836df650e0..6e6c6fdfb859 100644
--- a/frontend/src/components/QueryBuilderV2/QueryV2/QueryAggregation/QueryAggregationSelect.tsx
+++ b/frontend/src/components/QueryBuilderV2/QueryV2/QueryAggregation/QueryAggregationSelect.tsx
@@ -13,6 +13,7 @@ import {
CompletionContext,
completionKeymap,
CompletionResult,
+ startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { RangeSetBuilder } from '@codemirror/state';
@@ -28,7 +29,7 @@ import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
-import { useEffect, useMemo, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
@@ -122,6 +123,16 @@ function QueryAggregationSelect(): JSX.Element {
{ func: string; arg: string }[]
>([]);
const editorRef = useRef(null);
+ const [isFocused, setIsFocused] = useState(false);
+
+ // Helper function to safely start completion
+ const safeStartCompletion = useCallback((): void => {
+ requestAnimationFrame(() => {
+ if (editorRef.current) {
+ startCompletion(editorRef.current);
+ }
+ });
+ }, []);
// Update cursor position on every editor update
const handleUpdate = (update: { view: EditorView }): void => {
@@ -129,6 +140,13 @@ function QueryAggregationSelect(): JSX.Element {
setCursorPos(pos);
};
+ // Effect to handle focus state and trigger suggestions
+ useEffect(() => {
+ if (isFocused) {
+ safeStartCompletion();
+ }
+ }, [isFocused, safeStartCompletion]);
+
// Extract all valid function-argument pairs from the input
useEffect(() => {
const pairs: { func: string; arg: string }[] = [];
@@ -245,6 +263,11 @@ function QueryAggregationSelect(): JSX.Element {
changes: { from, to, insert: insertText },
selection: { anchor: cursorPos },
});
+
+ // Trigger suggestions after a small delay
+ setTimeout(() => {
+ safeStartCompletion();
+ }, 50);
},
}),
);
@@ -263,14 +286,20 @@ function QueryAggregationSelect(): JSX.Element {
from: number,
to: number,
): void => {
+ // Insert the selected key followed by ') '
view.dispatch({
- changes: { from, to, insert: completion.label },
- selection: { anchor: from + completion.label.length },
+ changes: { from, to, insert: `${completion.label}) ` },
+ selection: { anchor: from + completion.label.length + 2 }, // Position cursor after ') '
});
+
+ // Trigger next suggestions after a small delay
+ setTimeout(() => {
+ safeStartCompletion();
+ }, 50);
},
}),
) || [],
- [aggregateAttributeData],
+ [aggregateAttributeData, safeStartCompletion],
);
const aggregatorAutocomplete = useMemo(
@@ -349,21 +378,27 @@ function QueryAggregationSelect(): JSX.Element {
};
}
- // Before returning operatorCompletions, filter out 'count' if already present in the input (case-insensitive, direct text check)
+ // Show operator suggestions if no function context or not accepting args
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;
+
+ // Get the word before cursor if any
const word = context.matchBefore(/[\w\d_]+/);
- if (!word && !context.explicit) {
- return null;
+
+ // Show suggestions if:
+ // 1. There's a word match
+ // 2. The input is empty (cursor at start)
+ // 3. The user explicitly triggered completion
+ if (word || cursorPos === 0 || context.explicit) {
+ return {
+ from: word ? word.from : cursorPos,
+ options: availableOperators,
+ };
}
- return {
- from: word ? word.from : context.pos,
- options: availableOperators,
- };
}
return null;
@@ -383,7 +418,6 @@ function QueryAggregationSelect(): JSX.Element {
value={input}
onChange={setInput}
className="query-aggregation-select-editor"
- width="100%"
theme={copilot}
extensions={[
chipPlugin,
@@ -404,7 +438,16 @@ function QueryAggregationSelect(): JSX.Element {
completionKeymap: true,
}}
onUpdate={handleUpdate}
- ref={editorRef}
+ onCreateEditor={(view: EditorView): void => {
+ editorRef.current = view;
+ }}
+ onFocus={(): void => {
+ setIsFocused(true);
+ safeStartCompletion();
+ }}
+ onBlur={(): void => {
+ setIsFocused(false);
+ }}
/>
);
diff --git a/frontend/src/container/TracesExplorer/QuerySection/index.tsx b/frontend/src/container/TracesExplorer/QuerySection/index.tsx
index 4b957b67751c..b9b90d2d1023 100644
--- a/frontend/src/container/TracesExplorer/QuerySection/index.tsx
+++ b/frontend/src/container/TracesExplorer/QuerySection/index.tsx
@@ -7,8 +7,6 @@ import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQ
import { memo, useCallback, useMemo } from 'react';
import { DataSource } from 'types/common/queryBuilder';
-import { Container } from './styles';
-
function QuerySection(): JSX.Element {
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
@@ -40,19 +38,17 @@ function QuerySection(): JSX.Element {
}, [panelTypes, renderOrderBy]);
return (
-
-
-
+
);
}