mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 22:39:01 +08:00
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:
parent
5dc5b2e366
commit
6b096576ee
@ -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: "",
|
||||
},
|
||||
}
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
Loading…
x
Reference in New Issue
Block a user