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 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: "",
},
}

View File

@ -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',
}

View File

@ -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 && (
<div className="qb-search-view-container">
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
{isSearchV2Enabled ? (
<QueryBuilderSearchV2
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
) : (
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
)}
</div>
)}

View File

@ -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}
/>
</Select.Option>
);

View File

@ -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);
}
}
}
}

View File

@ -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<boolean>(false);
return (
@ -58,13 +69,21 @@ function Suggestions(props: ISuggestionsProps): React.ReactElement {
) : (
<Tooltip title={truncated ? label : ''} placement="topLeft">
<div className="container-without-tag">
<div className="dot" />
<Typography.Text
className={cx('text value', option)}
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
>
{`${label}`}
</Typography.Text>
<section className="left">
<div className="dot" />
<Typography.Text
className={cx('text value', option)}
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
>
{`${label}`}
</Typography.Text>
</section>
<section className="right">
{dataType && (
<Typography.Text className="data-type">{dataType}</Typography.Text>
)}
{tagValue.includes(label) && <Check size={14} />}
</section>
</div>
</Tooltip>
)}