feat: consume the new search bar (#5728)

* feat: consume the new search bar

* fix: minor css issue

* chore: address review comments

* fix: very fast typing

* chore: added inline code comments

* chore: add the changes behind FF
This commit is contained in:
Vikrant Gupta 2024-08-30 17:50:28 +05:30 committed by GitHub
parent 5dc5b2e366
commit 6b096576ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 187 additions and 61 deletions

View File

@ -13,6 +13,7 @@ const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT" const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY" const Gateway = "GATEWAY"
const PremiumSupport = "PREMIUM_SUPPORT" const PremiumSupport = "PREMIUM_SUPPORT"
const QueryBuilderSearchV2 = "QUERY_BUILDER_SEARCH_V2"
var BasicPlan = basemodel.FeatureSet{ var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{ basemodel.Feature{
@ -127,6 +128,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }
var ProPlan = basemodel.FeatureSet{ var ProPlan = basemodel.FeatureSet{
@ -235,6 +243,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }
var EnterprisePlan = basemodel.FeatureSet{ var EnterprisePlan = basemodel.FeatureSet{
@ -357,4 +372,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: QueryBuilderSearchV2,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }

View File

@ -21,4 +21,5 @@ export enum FeatureKeys {
CHAT_SUPPORT = 'CHAT_SUPPORT', CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY', GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT', PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
} }

View File

@ -1,5 +1,6 @@
import './LogsExplorerQuerySection.styles.scss'; import './LogsExplorerQuerySection.styles.scss';
import { FeatureKeys } from 'constants/features';
import { import {
initialQueriesMap, initialQueriesMap,
OPERATORS, OPERATORS,
@ -9,11 +10,13 @@ import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { QueryBuilder } from 'container/QueryBuilder'; import { QueryBuilder } from 'container/QueryBuilder';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces'; import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces'; import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { import {
prepareQueryWithDefaultTimestamp, prepareQueryWithDefaultTimestamp,
SELECTED_VIEWS, SELECTED_VIEWS,
@ -86,15 +89,26 @@ function LogExplorerQuerySection({
[handleChangeQueryData], [handleChangeQueryData],
); );
const isSearchV2Enabled =
useFeatureFlags(FeatureKeys.QUERY_BUILDER_SEARCH_V2)?.active || false;
return ( return (
<> <>
{selectedView === SELECTED_VIEWS.SEARCH && ( {selectedView === SELECTED_VIEWS.SEARCH && (
<div className="qb-search-view-container"> <div className="qb-search-view-container">
<QueryBuilderSearch {isSearchV2Enabled ? (
query={query} <QueryBuilderSearchV2
onChange={handleChangeTagFilters} query={query}
whereClauseConfig={filterConfigs?.filters} onChange={handleChangeTagFilters}
/> whereClauseConfig={filterConfigs?.filters}
/>
) : (
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
)}
</div> </div>
)} )}

View File

@ -56,7 +56,11 @@ import { v4 as uuid } from 'uuid';
import { selectStyle } from '../QueryBuilderSearch/config'; import { selectStyle } from '../QueryBuilderSearch/config';
import { PLACEHOLDER } from '../QueryBuilderSearch/constant'; import { PLACEHOLDER } from '../QueryBuilderSearch/constant';
import { TypographyText } from '../QueryBuilderSearch/style'; import { TypographyText } from '../QueryBuilderSearch/style';
import { getTagToken, isInNInOperator } from '../QueryBuilderSearch/utils'; import {
checkCommaInValue,
getTagToken,
isInNInOperator,
} from '../QueryBuilderSearch/utils';
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown'; import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
import Suggestions from './Suggestions'; import Suggestions from './Suggestions';
@ -213,18 +217,11 @@ function QueryBuilderSearchV2(
const isQueryEnabled = useMemo(() => { const isQueryEnabled = useMemo(() => {
if (currentState === DropdownState.ATTRIBUTE_KEY) { if (currentState === DropdownState.ATTRIBUTE_KEY) {
return query.dataSource === DataSource.METRICS return query.dataSource === DataSource.METRICS
? !!query.aggregateOperator && ? !!query.dataSource && !!query.aggregateAttribute.dataType
!!query.dataSource &&
!!query.aggregateAttribute.dataType
: true; : true;
} }
return false; return false;
}, [ }, [currentState, query.aggregateAttribute.dataType, query.dataSource]);
currentState,
query.aggregateAttribute.dataType,
query.aggregateOperator,
query.dataSource,
]);
const { data, isFetching } = useGetAggregateKeys( const { data, isFetching } = useGetAggregateKeys(
{ {
@ -324,6 +321,23 @@ function QueryBuilderSearchV2(
if (isMulti) { if (isMulti) {
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue); 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]; const newSearch = [...tagValue];
newSearch[newSearch.length === 0 ? 0 : newSearch.length - 1] = value; newSearch[newSearch.length === 0 ? 0 : newSearch.length - 1] = value;
const newSearchValue = newSearch.join(','); const newSearchValue = newSearch.join(',');
@ -356,6 +370,7 @@ function QueryBuilderSearchV2(
event.stopPropagation(); event.stopPropagation();
setTags((prev) => prev.slice(0, -1)); setTags((prev) => prev.slice(0, -1));
} }
if ((event.ctrlKey || event.metaKey) && event.key === '/') { if ((event.ctrlKey || event.metaKey) && event.key === '/') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -375,6 +390,7 @@ function QueryBuilderSearchV2(
if (searchValue) { if (searchValue) {
const operatorType = const operatorType =
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID'; operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
// if key is added and operator is not present then convert to body CONTAINS key
if ( if (
currentFilterItem?.key && currentFilterItem?.key &&
isEmpty(currentFilterItem?.op) && isEmpty(currentFilterItem?.op) &&
@ -403,6 +419,7 @@ function QueryBuilderSearchV2(
currentFilterItem?.op === OPERATORS.EXISTS || currentFilterItem?.op === OPERATORS.EXISTS ||
currentFilterItem?.op === OPERATORS.NOT_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) => [ setTags((prev) => [
...prev, ...prev,
{ {
@ -415,6 +432,7 @@ function QueryBuilderSearchV2(
setSearchValue(''); setSearchValue('');
setCurrentState(DropdownState.ATTRIBUTE_KEY); setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else if ( } else if (
// if the current state is in sync with the kind of operator used then convert into a tag
validationMapper[operatorType]?.( validationMapper[operatorType]?.(
isArray(currentFilterItem?.value) isArray(currentFilterItem?.value)
? currentFilterItem?.value.length || 0 ? currentFilterItem?.value.length || 0
@ -445,15 +463,21 @@ function QueryBuilderSearchV2(
// this useEffect takes care of tokenisation based on the search state // this useEffect takes care of tokenisation based on the search state
useEffect(() => { 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) { if (isFetchingSuggestions) {
return; return;
} }
// if there is no search value reset to the default state
if (!searchValue) { if (!searchValue) {
setCurrentFilterItem(undefined); setCurrentFilterItem(undefined);
setCurrentState(DropdownState.ATTRIBUTE_KEY); setCurrentState(DropdownState.ATTRIBUTE_KEY);
} }
// split the current search value based on delimiters
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue); const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
// Case 1 -> when typing an attribute key (not selecting from dropdown)
if (tagKey && isUndefined(currentFilterItem?.key)) { if (tagKey && isUndefined(currentFilterItem?.key)) {
let currentRunningAttributeKey; let currentRunningAttributeKey;
const isSuggestedKeyInAutocomplete = suggestionsData?.payload?.attributes?.some( const isSuggestedKeyInAutocomplete = suggestionsData?.payload?.attributes?.some(
@ -470,8 +494,17 @@ function QueryBuilderSearchV2(
[currentRunningAttributeKey] = allAttributesMatchingTheKey; [currentRunningAttributeKey] = allAttributesMatchingTheKey;
} }
if (allAttributesMatchingTheKey?.length > 1) { if (allAttributesMatchingTheKey?.length > 1) {
// the priority logic goes here // when there are multiple options let the user choose it until they do not select an operator
[currentRunningAttributeKey] = allAttributesMatchingTheKey; 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) { if (currentRunningAttributeKey) {
@ -488,7 +521,6 @@ function QueryBuilderSearchV2(
setCurrentFilterItem({ setCurrentFilterItem({
key: { key: {
key: tagKey.split(' ')[0], 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, dataType: DataTypes.EMPTY,
type: '', type: '',
isColumn: false, isColumn: false,
@ -500,12 +532,15 @@ function QueryBuilderSearchV2(
setCurrentState(DropdownState.OPERATOR); setCurrentState(DropdownState.OPERATOR);
} }
} else if ( } 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 &&
currentFilterItem?.key?.key !== tagKey.split(' ')[0] currentFilterItem?.key?.key !== tagKey.split(' ')[0]
) { ) {
setCurrentFilterItem(undefined); setCurrentFilterItem(undefined);
setCurrentState(DropdownState.ATTRIBUTE_KEY); setCurrentState(DropdownState.ATTRIBUTE_KEY);
} else if (tagOperator && isEmpty(currentFilterItem?.op)) { } else if (tagOperator && isEmpty(currentFilterItem?.op)) {
// Case 3 -> key is set and now typing for the operator
if ( if (
tagOperator === OPERATORS.EXISTS || tagOperator === OPERATORS.EXISTS ||
tagOperator === OPERATORS.NOT_EXISTS tagOperator === OPERATORS.NOT_EXISTS
@ -531,6 +566,7 @@ function QueryBuilderSearchV2(
setCurrentState(DropdownState.ATTRIBUTE_VALUE); setCurrentState(DropdownState.ATTRIBUTE_VALUE);
} }
} else if ( } else if (
// Case 4 -> selected operator from dropdown and then erased a part of it
!isEmpty(currentFilterItem?.op) && !isEmpty(currentFilterItem?.op) &&
tagOperator !== currentFilterItem?.op tagOperator !== currentFilterItem?.op
) { ) {
@ -540,10 +576,12 @@ function QueryBuilderSearchV2(
value: '', value: '',
})); }));
setCurrentState(DropdownState.OPERATOR); 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 = { const currentValue = {
key: currentFilterItem?.key as BaseAutocompleteData, key: currentFilterItem?.key as BaseAutocompleteData,
operator: currentFilterItem?.op as string, op: currentFilterItem?.op as string,
value: tagValue, value: tagValue,
}; };
if (!isEqual(currentValue, currentFilterItem)) { if (!isEqual(currentValue, currentFilterItem)) {
@ -561,6 +599,7 @@ function QueryBuilderSearchV2(
suggestionsData?.payload?.attributes, suggestionsData?.payload?.attributes,
searchValue, searchValue,
isFetchingSuggestions, isFetchingSuggestions,
currentState,
]); ]);
// the useEffect takes care of setting the dropdown values correctly on change of the current state // 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) { if (currentState === DropdownState.ATTRIBUTE_VALUE) {
const values: string[] = const values: string[] = [];
Object.values(attributeValues?.payload || {}).find((el) => !!el) || [];
const { tagValue } = getTagToken(searchValue); 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) { values.push(
if (isArray(tagValue)) { ...(Object.values(attributeValues?.payload || {}).find((el) => !!el) || []),
if (!isEmpty(tagValue[tagValue.length - 1])) );
values.push(tagValue[tagValue.length - 1]);
} else if (!isEmpty(tagValue)) values.push(tagValue);
}
setDropdownOptions( setDropdownOptions(
values.map((val) => ({ values.map((val) => ({
label: val, label: checkCommaInValue(String(val)),
value: val, value: val,
})), })),
); );
} }
}, [ }, [
attributeValues?.payload, attributeValues?.payload,
currentFilterItem?.key.dataType, currentFilterItem?.key?.dataType,
currentState, currentState,
data?.payload?.attributeKeys, data?.payload?.attributeKeys,
isLogsExplorerPage, isLogsExplorerPage,
@ -674,7 +712,15 @@ function QueryBuilderSearchV2(
onChange(filterTags); onChange(filterTags);
setTags(filterTags.items as ITag[]); 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( const isLastQuery = useMemo(
() => () =>
@ -843,6 +889,7 @@ function QueryBuilderSearchV2(
label={option.label} label={option.label}
value={option.value} value={option.value}
option={currentState} option={currentState}
searchValue={searchValue}
/> />
</Select.Option> </Select.Option>
); );

View File

@ -105,29 +105,52 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
justify-content: space-between;
.OPERATOR { .left {
color: var(--bg-vanilla-400); display: flex;
font-family: 'Space Mono'; align-items: center;
font-size: 14px; gap: 8px;
font-style: normal;
font-weight: 500; .OPERATOR {
line-height: 20px; /* 142.857% */ color: var(--bg-vanilla-400);
letter-spacing: -0.07px; font-family: 'Space Mono';
text-transform: uppercase; font-size: 14px;
width: 100%; 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 { .right {
color: var(--bg-vanilla-400); display: flex;
font-family: 'Space Mono'; align-items: center;
font-size: 14px; gap: 4px;
font-style: normal; .data-type {
font-weight: 500; display: flex;
line-height: 20px; /* 142.857% */ height: 20px;
letter-spacing: -0.07px; padding: 4px 8px;
text-transform: uppercase; justify-content: center;
width: 100%; align-items: center;
gap: 4px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.08);
}
} }
} }
} }

View File

@ -4,20 +4,22 @@ import { Color } from '@signozhq/design-tokens';
import { Tooltip, Typography } from 'antd'; import { Tooltip, Typography } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import { isEmpty, isObject } from 'lodash-es'; import { isEmpty, isObject } from 'lodash-es';
import { Zap } from 'lucide-react'; import { Check, Zap } from 'lucide-react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { getTagToken } from '../QueryBuilderSearch/utils';
import { DropdownState } from './QueryBuilderSearchV2'; import { DropdownState } from './QueryBuilderSearchV2';
interface ISuggestionsProps { interface ISuggestionsProps {
label: string; label: string;
value: BaseAutocompleteData | string; value: BaseAutocompleteData | string;
option: DropdownState; option: DropdownState;
searchValue: string;
} }
function Suggestions(props: ISuggestionsProps): React.ReactElement { function Suggestions(props: ISuggestionsProps): React.ReactElement {
const { label, value, option } = props; const { label, value, option, searchValue } = props;
const optionType = useMemo(() => { const optionType = useMemo(() => {
if (isObject(value)) { if (isObject(value)) {
@ -26,6 +28,15 @@ function Suggestions(props: ISuggestionsProps): React.ReactElement {
return ''; return '';
}, [value]); }, [value]);
const dataType = useMemo(() => {
if (isObject(value)) {
return value.dataType;
}
return '';
}, [value]);
const { tagValue } = getTagToken(searchValue);
const [truncated, setTruncated] = useState<boolean>(false); const [truncated, setTruncated] = useState<boolean>(false);
return ( return (
@ -58,13 +69,21 @@ function Suggestions(props: ISuggestionsProps): React.ReactElement {
) : ( ) : (
<Tooltip title={truncated ? label : ''} placement="topLeft"> <Tooltip title={truncated ? label : ''} placement="topLeft">
<div className="container-without-tag"> <div className="container-without-tag">
<div className="dot" /> <section className="left">
<Typography.Text <div className="dot" />
className={cx('text value', option)} <Typography.Text
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }} className={cx('text value', option)}
> ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
{`${label}`} >
</Typography.Text> {`${label}`}
</Typography.Text>
</section>
<section className="right">
{dataType && (
<Typography.Text className="data-type">{dataType}</Typography.Text>
)}
{tagValue.includes(label) && <Check size={14} />}
</section>
</div> </div>
</Tooltip> </Tooltip>
)} )}