mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-23 02:17:11 +00:00
feat: improve having suggestions
This commit is contained in:
parent
7e18087db6
commit
0c9f06850a
@ -6,6 +6,7 @@ import {
|
|||||||
CompletionContext,
|
CompletionContext,
|
||||||
completionKeymap,
|
completionKeymap,
|
||||||
CompletionResult,
|
CompletionResult,
|
||||||
|
startCompletion,
|
||||||
} from '@codemirror/autocomplete';
|
} from '@codemirror/autocomplete';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||||
@ -50,6 +51,17 @@ const havingOperators = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add common value suggestions
|
||||||
|
const commonValues = [
|
||||||
|
{ label: '0', value: '0' },
|
||||||
|
{ label: '1', value: '1' },
|
||||||
|
{ label: '5', value: '5' },
|
||||||
|
{ label: '10', value: '10' },
|
||||||
|
{ label: '50', value: '50' },
|
||||||
|
{ label: '100', value: '100' },
|
||||||
|
{ label: '1000', value: '1000' },
|
||||||
|
];
|
||||||
|
|
||||||
const conjunctions = [
|
const conjunctions = [
|
||||||
{ label: 'AND', value: 'AND' },
|
{ label: 'AND', value: 'AND' },
|
||||||
{ label: 'OR', value: 'OR' },
|
{ label: 'OR', value: 'OR' },
|
||||||
@ -58,44 +70,88 @@ const conjunctions = [
|
|||||||
function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
|
function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
|
||||||
const { aggregationOptions } = useQueryBuilderV2Context();
|
const { aggregationOptions } = useQueryBuilderV2Context();
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
const editorRef = useRef<EditorView | null>(null);
|
const editorRef = useRef<EditorView | null>(null);
|
||||||
|
|
||||||
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
|
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
|
||||||
|
|
||||||
|
// Effect to handle focus state and trigger suggestions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const options = [];
|
if (isFocused && editorRef.current && options.length > 0) {
|
||||||
|
startCompletion(editorRef.current);
|
||||||
|
}
|
||||||
|
}, [isFocused, options]);
|
||||||
|
|
||||||
|
// Update options when aggregation options change
|
||||||
|
useEffect(() => {
|
||||||
|
const newOptions = [];
|
||||||
for (let i = 0; i < aggregationOptions.length; i++) {
|
for (let i = 0; i < aggregationOptions.length; i++) {
|
||||||
const opt = aggregationOptions[i];
|
const opt = aggregationOptions[i];
|
||||||
|
|
||||||
for (let j = 0; j < havingOperators.length; j++) {
|
for (let j = 0; j < havingOperators.length; j++) {
|
||||||
const operator = havingOperators[j];
|
const operator = havingOperators[j];
|
||||||
|
newOptions.push({
|
||||||
options.push({
|
label: `${opt.func}(${opt.arg}) ${operator.label}`,
|
||||||
label: `${opt.func}(${opt.arg}) ${operator.label} `,
|
|
||||||
value: `${opt.func}(${opt.arg}) ${operator.label} `,
|
value: `${opt.func}(${opt.arg}) ${operator.label} `,
|
||||||
|
apply: (
|
||||||
|
view: EditorView,
|
||||||
|
completion: { label: string; value: string },
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
): void => {
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from, to, insert: completion.value },
|
||||||
|
selection: { anchor: from + completion.value.length },
|
||||||
|
});
|
||||||
|
// Trigger value suggestions immediately after operator
|
||||||
|
setTimeout(() => {
|
||||||
|
startCompletion(view);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setOptions(newOptions);
|
||||||
setOptions(options);
|
|
||||||
}, [aggregationOptions]);
|
}, [aggregationOptions]);
|
||||||
|
|
||||||
// Helper to check if a string is a number
|
// Helper to check if a string is a number
|
||||||
const isNumber = (token: string): boolean => /^-?\d+(\.\d+)?$/.test(token);
|
const isNumber = (token: string): boolean => /^-?\d+(\.\d+)?$/.test(token);
|
||||||
|
|
||||||
const havingAutocomplete = useMemo(() => {
|
// Helper to check if we're after an operator
|
||||||
const isKeyOperator = (token: string): boolean =>
|
const isAfterOperator = (tokens: string[]): boolean => {
|
||||||
options.some((opt) => token.startsWith(opt.value));
|
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)
|
||||||
|
return havingOperators.some((op) => {
|
||||||
|
const opWithSpace = `${op.value} `;
|
||||||
|
return lastToken.endsWith(op.value) || lastToken.endsWith(opWithSpace);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return autocompletion({
|
const havingAutocomplete = useMemo(
|
||||||
|
() =>
|
||||||
|
autocompletion({
|
||||||
override: [
|
override: [
|
||||||
(context: CompletionContext): CompletionResult | null => {
|
(context: CompletionContext): CompletionResult | null => {
|
||||||
const text = context.state.sliceDoc(0, context.pos);
|
const text = context.state.sliceDoc(0, context.pos);
|
||||||
const trimmedText = text.trim();
|
const trimmedText = text.trim();
|
||||||
const tokens = trimmedText.split(/\s+/).filter(Boolean);
|
const tokens = trimmedText.split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
|
// Handle empty state when no aggregation options are available
|
||||||
|
if (options.length === 0) {
|
||||||
|
return {
|
||||||
|
from: context.pos,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label:
|
||||||
|
'No aggregation functions available. Please add aggregation functions first.',
|
||||||
|
type: 'text',
|
||||||
|
apply: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Suggest key/operator pairs and ( for grouping
|
// Suggest key/operator pairs and ( for grouping
|
||||||
if (
|
if (
|
||||||
tokens.length === 0 ||
|
tokens.length === 0 ||
|
||||||
@ -107,12 +163,24 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
|
|||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isKeyOperator(tokens[tokens.length - 1])) {
|
|
||||||
|
// Show value suggestions after operator
|
||||||
|
if (isAfterOperator(tokens)) {
|
||||||
return {
|
return {
|
||||||
from: context.pos,
|
from: context.pos,
|
||||||
options: [{ label: 'Enter a number value', type: 'text', apply: '' }],
|
options: [
|
||||||
|
...commonValues,
|
||||||
|
{
|
||||||
|
label: 'Enter a custom number value',
|
||||||
|
type: 'text',
|
||||||
|
apply: (): boolean =>
|
||||||
|
// Don't insert any text, just let the user type
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suggest ) for grouping after a value and a space, if there are unmatched (
|
// Suggest ) for grouping after a value and a space, if there are unmatched (
|
||||||
if (
|
if (
|
||||||
tokens.length > 0 &&
|
tokens.length > 0 &&
|
||||||
@ -124,6 +192,7 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
|
|||||||
options: conjunctions,
|
options: conjunctions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suggest conjunctions after a closing parenthesis and a space
|
// Suggest conjunctions after a closing parenthesis and a space
|
||||||
if (
|
if (
|
||||||
tokens.length > 0 &&
|
tokens.length > 0 &&
|
||||||
@ -135,6 +204,7 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
|
|||||||
options: conjunctions,
|
options: conjunctions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -142,8 +212,9 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
|
|||||||
closeOnBlur: false,
|
closeOnBlur: false,
|
||||||
maxRenderedOptions: 50,
|
maxRenderedOptions: 50,
|
||||||
activateOnTyping: true,
|
activateOnTyping: true,
|
||||||
});
|
}),
|
||||||
}, [options]);
|
[options],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="having-filter-container">
|
<div className="having-filter-container">
|
||||||
@ -171,7 +242,18 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
|
|||||||
autocompletion: true,
|
autocompletion: true,
|
||||||
completionKeymap: true,
|
completionKeymap: true,
|
||||||
}}
|
}}
|
||||||
ref={editorRef}
|
onCreateEditor={(view: EditorView): void => {
|
||||||
|
editorRef.current = view;
|
||||||
|
}}
|
||||||
|
onFocus={(): void => {
|
||||||
|
setIsFocused(true);
|
||||||
|
if (editorRef.current) {
|
||||||
|
startCompletion(editorRef.current);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(): void => {
|
||||||
|
setIsFocused(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="close-btn periscope-btn ghost"
|
className="close-btn periscope-btn ghost"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user