diff --git a/ee/query-service/model/plans.go b/ee/query-service/model/plans.go
index 9b696c013f..dbd8b56965 100644
--- a/ee/query-service/model/plans.go
+++ b/ee/query-service/model/plans.go
@@ -13,6 +13,7 @@ const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT"
+const QueryBuilderSearchV2 = "QUERY_BUILDER_SEARCH_V2"
var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{
@@ -127,6 +128,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
+ basemodel.Feature{
+ Name: QueryBuilderSearchV2,
+ Active: false,
+ Usage: 0,
+ UsageLimit: -1,
+ Route: "",
+ },
}
var ProPlan = basemodel.FeatureSet{
@@ -235,6 +243,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
+ basemodel.Feature{
+ Name: QueryBuilderSearchV2,
+ Active: false,
+ Usage: 0,
+ UsageLimit: -1,
+ Route: "",
+ },
}
var EnterprisePlan = basemodel.FeatureSet{
@@ -357,4 +372,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1,
Route: "",
},
+ basemodel.Feature{
+ Name: QueryBuilderSearchV2,
+ Active: false,
+ Usage: 0,
+ UsageLimit: -1,
+ Route: "",
+ },
}
diff --git a/frontend/src/constants/features.ts b/frontend/src/constants/features.ts
index bdacdb057b..769522455d 100644
--- a/frontend/src/constants/features.ts
+++ b/frontend/src/constants/features.ts
@@ -21,4 +21,5 @@ export enum FeatureKeys {
CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
+ QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
}
diff --git a/frontend/src/container/LogExplorerQuerySection/index.tsx b/frontend/src/container/LogExplorerQuerySection/index.tsx
index 1eea60da47..f807103f68 100644
--- a/frontend/src/container/LogExplorerQuerySection/index.tsx
+++ b/frontend/src/container/LogExplorerQuerySection/index.tsx
@@ -1,5 +1,6 @@
import './LogsExplorerQuerySection.styles.scss';
+import { FeatureKeys } from 'constants/features';
import {
initialQueriesMap,
OPERATORS,
@@ -9,11 +10,13 @@ import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { QueryBuilder } from 'container/QueryBuilder';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
+import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
+import useFeatureFlags from 'hooks/useFeatureFlag';
import {
prepareQueryWithDefaultTimestamp,
SELECTED_VIEWS,
@@ -86,15 +89,26 @@ function LogExplorerQuerySection({
[handleChangeQueryData],
);
+ const isSearchV2Enabled =
+ useFeatureFlags(FeatureKeys.QUERY_BUILDER_SEARCH_V2)?.active || false;
+
return (
<>
{selectedView === SELECTED_VIEWS.SEARCH && (
-
+ {isSearchV2Enabled ? (
+
+ ) : (
+
+ )}
)}
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx
index d53a517c48..4c800e7d6d 100644
--- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx
+++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx
@@ -56,7 +56,11 @@ import { v4 as uuid } from 'uuid';
import { selectStyle } from '../QueryBuilderSearch/config';
import { PLACEHOLDER } from '../QueryBuilderSearch/constant';
import { TypographyText } from '../QueryBuilderSearch/style';
-import { getTagToken, isInNInOperator } from '../QueryBuilderSearch/utils';
+import {
+ checkCommaInValue,
+ getTagToken,
+ isInNInOperator,
+} from '../QueryBuilderSearch/utils';
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
import Suggestions from './Suggestions';
@@ -213,18 +217,11 @@ function QueryBuilderSearchV2(
const isQueryEnabled = useMemo(() => {
if (currentState === DropdownState.ATTRIBUTE_KEY) {
return query.dataSource === DataSource.METRICS
- ? !!query.aggregateOperator &&
- !!query.dataSource &&
- !!query.aggregateAttribute.dataType
+ ? !!query.dataSource && !!query.aggregateAttribute.dataType
: true;
}
return false;
- }, [
- currentState,
- query.aggregateAttribute.dataType,
- query.aggregateOperator,
- query.dataSource,
- ]);
+ }, [currentState, query.aggregateAttribute.dataType, query.dataSource]);
const { data, isFetching } = useGetAggregateKeys(
{
@@ -324,6 +321,23 @@ function QueryBuilderSearchV2(
if (isMulti) {
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
+ // this condition takes care of adding the IN/NIN multi values when pressed enter on an already existing value.
+ // not the best interaction but in sync with what we have today!
+ if (tagValue.includes(String(value))) {
+ setSearchValue('');
+ setCurrentState(DropdownState.ATTRIBUTE_KEY);
+ setCurrentFilterItem(undefined);
+ setTags((prev) => [
+ ...prev,
+ {
+ key: currentFilterItem?.key,
+ op: currentFilterItem?.op,
+ value: tagValue.join(','),
+ } as ITag,
+ ]);
+ return;
+ }
+ // this is for adding subsequent comma seperated values
const newSearch = [...tagValue];
newSearch[newSearch.length === 0 ? 0 : newSearch.length - 1] = value;
const newSearchValue = newSearch.join(',');
@@ -356,6 +370,7 @@ function QueryBuilderSearchV2(
event.stopPropagation();
setTags((prev) => prev.slice(0, -1));
}
+
if ((event.ctrlKey || event.metaKey) && event.key === '/') {
event.preventDefault();
event.stopPropagation();
@@ -375,6 +390,7 @@ function QueryBuilderSearchV2(
if (searchValue) {
const operatorType =
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
+ // if key is added and operator is not present then convert to body CONTAINS key
if (
currentFilterItem?.key &&
isEmpty(currentFilterItem?.op) &&
@@ -403,6 +419,7 @@ function QueryBuilderSearchV2(
currentFilterItem?.op === OPERATORS.EXISTS ||
currentFilterItem?.op === OPERATORS.NOT_EXISTS
) {
+ // is exists and not exists operator is present then convert directly to tag! no need of value here
setTags((prev) => [
...prev,
{
@@ -415,6 +432,7 @@ function QueryBuilderSearchV2(
setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else if (
+ // if the current state is in sync with the kind of operator used then convert into a tag
validationMapper[operatorType]?.(
isArray(currentFilterItem?.value)
? currentFilterItem?.value.length || 0
@@ -445,15 +463,21 @@ function QueryBuilderSearchV2(
// this useEffect takes care of tokenisation based on the search state
useEffect(() => {
+ // if we are still fetching the suggestions then return as we won't know the type / data-type etc for the attribute key
if (isFetchingSuggestions) {
return;
}
+
+ // if there is no search value reset to the default state
if (!searchValue) {
setCurrentFilterItem(undefined);
setCurrentState(DropdownState.ATTRIBUTE_KEY);
}
+
+ // split the current search value based on delimiters
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
+ // Case 1 -> when typing an attribute key (not selecting from dropdown)
if (tagKey && isUndefined(currentFilterItem?.key)) {
let currentRunningAttributeKey;
const isSuggestedKeyInAutocomplete = suggestionsData?.payload?.attributes?.some(
@@ -470,8 +494,17 @@ function QueryBuilderSearchV2(
[currentRunningAttributeKey] = allAttributesMatchingTheKey;
}
if (allAttributesMatchingTheKey?.length > 1) {
- // the priority logic goes here
- [currentRunningAttributeKey] = allAttributesMatchingTheKey;
+ // when there are multiple options let the user choose it until they do not select an operator
+ if (tagOperator) {
+ // if they select the operator then pick the first one from the ranked list
+ setCurrentFilterItem({
+ key: allAttributesMatchingTheKey?.[0],
+ op: tagOperator,
+ value: '',
+ });
+ setCurrentState(DropdownState.ATTRIBUTE_VALUE);
+ }
+ return;
}
if (currentRunningAttributeKey) {
@@ -488,7 +521,6 @@ function QueryBuilderSearchV2(
setCurrentFilterItem({
key: {
key: tagKey.split(' ')[0],
- // update this for has and nhas operator , check the useEffect of source keys in older component for details
dataType: DataTypes.EMPTY,
type: '',
isColumn: false,
@@ -500,12 +532,15 @@ function QueryBuilderSearchV2(
setCurrentState(DropdownState.OPERATOR);
}
} else if (
+ // Case 2 - if key is defined but the search text doesn't match with the set key,
+ // can happen when user selects from dropdown and then deletes a few characters
currentFilterItem?.key &&
currentFilterItem?.key?.key !== tagKey.split(' ')[0]
) {
setCurrentFilterItem(undefined);
setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else if (tagOperator && isEmpty(currentFilterItem?.op)) {
+ // Case 3 -> key is set and now typing for the operator
if (
tagOperator === OPERATORS.EXISTS ||
tagOperator === OPERATORS.NOT_EXISTS
@@ -531,6 +566,7 @@ function QueryBuilderSearchV2(
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
}
} else if (
+ // Case 4 -> selected operator from dropdown and then erased a part of it
!isEmpty(currentFilterItem?.op) &&
tagOperator !== currentFilterItem?.op
) {
@@ -540,10 +576,12 @@ function QueryBuilderSearchV2(
value: '',
}));
setCurrentState(DropdownState.OPERATOR);
- } else if (!isEmpty(tagValue)) {
+ } else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
+ // Case 5 -> the final value state where we set the current filter values and the tokenisation happens on either
+ // dropdown click or blur event
const currentValue = {
key: currentFilterItem?.key as BaseAutocompleteData,
- operator: currentFilterItem?.op as string,
+ op: currentFilterItem?.op as string,
value: tagValue,
};
if (!isEqual(currentValue, currentFilterItem)) {
@@ -561,6 +599,7 @@ function QueryBuilderSearchV2(
suggestionsData?.payload?.attributes,
searchValue,
isFetchingSuggestions,
+ currentState,
]);
// the useEffect takes care of setting the dropdown values correctly on change of the current state
@@ -627,28 +666,27 @@ function QueryBuilderSearchV2(
}
if (currentState === DropdownState.ATTRIBUTE_VALUE) {
- const values: string[] =
- Object.values(attributeValues?.payload || {}).find((el) => !!el) || [];
-
+ const values: string[] = [];
const { tagValue } = getTagToken(searchValue);
+ if (isArray(tagValue)) {
+ if (!isEmpty(tagValue[tagValue.length - 1]))
+ values.push(tagValue[tagValue.length - 1]);
+ } else if (!isEmpty(tagValue)) values.push(tagValue);
- if (values.length === 0) {
- if (isArray(tagValue)) {
- if (!isEmpty(tagValue[tagValue.length - 1]))
- values.push(tagValue[tagValue.length - 1]);
- } else if (!isEmpty(tagValue)) values.push(tagValue);
- }
+ values.push(
+ ...(Object.values(attributeValues?.payload || {}).find((el) => !!el) || []),
+ );
setDropdownOptions(
values.map((val) => ({
- label: val,
+ label: checkCommaInValue(String(val)),
value: val,
})),
);
}
}, [
attributeValues?.payload,
- currentFilterItem?.key.dataType,
+ currentFilterItem?.key?.dataType,
currentState,
data?.payload?.attributeKeys,
isLogsExplorerPage,
@@ -674,7 +712,15 @@ function QueryBuilderSearchV2(
onChange(filterTags);
setTags(filterTags.items as ITag[]);
}
- }, [onChange, query.filters, tags]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [tags]);
+
+ useEffect(() => {
+ if (!isEqual(query.filters.items, tags)) {
+ setTags(getInitTags(query));
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [query]);
const isLastQuery = useMemo(
() =>
@@ -843,6 +889,7 @@ function QueryBuilderSearchV2(
label={option.label}
value={option.value}
option={currentState}
+ searchValue={searchValue}
/>
);
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss
index 362d6e4c6a..bd7ad36a5a 100644
--- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss
+++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.styles.scss
@@ -105,29 +105,52 @@
display: flex;
align-items: center;
gap: 8px;
+ justify-content: space-between;
- .OPERATOR {
- color: var(--bg-vanilla-400);
- font-family: 'Space Mono';
- font-size: 14px;
- font-style: normal;
- font-weight: 500;
- line-height: 20px; /* 142.857% */
- letter-spacing: -0.07px;
- text-transform: uppercase;
- width: 100%;
+ .left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .OPERATOR {
+ color: var(--bg-vanilla-400);
+ font-family: 'Space Mono';
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ text-transform: uppercase;
+ width: 100%;
+ }
+
+ .VALUE {
+ color: var(--bg-vanilla-400);
+ font-family: 'Space Mono';
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px; /* 142.857% */
+ letter-spacing: -0.07px;
+ text-transform: uppercase;
+ width: 100%;
+ }
}
- .VALUE {
- color: var(--bg-vanilla-400);
- font-family: 'Space Mono';
- font-size: 14px;
- font-style: normal;
- font-weight: 500;
- line-height: 20px; /* 142.857% */
- letter-spacing: -0.07px;
- text-transform: uppercase;
- width: 100%;
+ .right {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ .data-type {
+ display: flex;
+ height: 20px;
+ padding: 4px 8px;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+ border-radius: 20px;
+ background: rgba(255, 255, 255, 0.08);
+ }
}
}
}
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx
index 49d6040b7b..88507d80eb 100644
--- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx
+++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions.tsx
@@ -4,20 +4,22 @@ import { Color } from '@signozhq/design-tokens';
import { Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { isEmpty, isObject } from 'lodash-es';
-import { Zap } from 'lucide-react';
+import { Check, Zap } from 'lucide-react';
import { useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
+import { getTagToken } from '../QueryBuilderSearch/utils';
import { DropdownState } from './QueryBuilderSearchV2';
interface ISuggestionsProps {
label: string;
value: BaseAutocompleteData | string;
option: DropdownState;
+ searchValue: string;
}
function Suggestions(props: ISuggestionsProps): React.ReactElement {
- const { label, value, option } = props;
+ const { label, value, option, searchValue } = props;
const optionType = useMemo(() => {
if (isObject(value)) {
@@ -26,6 +28,15 @@ function Suggestions(props: ISuggestionsProps): React.ReactElement {
return '';
}, [value]);
+ const dataType = useMemo(() => {
+ if (isObject(value)) {
+ return value.dataType;
+ }
+ return '';
+ }, [value]);
+
+ const { tagValue } = getTagToken(searchValue);
+
const [truncated, setTruncated] = useState(false);
return (
@@ -58,13 +69,21 @@ function Suggestions(props: ISuggestionsProps): React.ReactElement {
) : (
-
-
setTruncated(ellipsis) }}
- >
- {`${label}`}
-
+
+
+ setTruncated(ellipsis) }}
+ >
+ {`${label}`}
+
+
+
+ {dataType && (
+ {dataType}
+ )}
+ {tagValue.includes(label) && }
+
)}