mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 01:49:05 +08:00
feat: rewrite the query builder search component (#5659)
* feat: make the query builder search extensible * feat: setup the framework and necessary states needed * feat: cover the happy path of selects * chore: forward typing flow handled * chore: add antd select * chore: add antd select * chore: handle forward and backward flows * feat: added tag properites to the search bar and multi tag partial handling * feat: handle tag on blur and body contains changes * feat: handle tag deselect * feat: multi tag handling * feat: multi tag handling * fix: jest test cases * chore: update the key * chore: add edit tag not working as expected * feat: handle cases for exists and nexists * fix: handle has / nhas operators * chore: fix usability issues * chore: remove the usage for the new bar * fix: flaky build issues * feat: client changes for consumption and design changes for where clause in logs explorer (#5712) * feat: query search new ui * feat: suggestions changes in v2 * feat: dropdown and tags ui touch up * feat: added missing keyboard shortcuts * fix: race condition issues * feat: remove usage * fix: operator select fix * fix: handle example queries click changes * chore: design sync * chore: handle boolean selects * chore: address review comments
This commit is contained in:
parent
98367fd054
commit
5796d6cb8c
@ -264,7 +264,8 @@ function LogsExplorerViews({
|
||||
undefined,
|
||||
listQueryKeyRef,
|
||||
{
|
||||
...(!isEmpty(queryId) && { 'X-SIGNOZ-QUERY-ID': queryId }),
|
||||
...(!isEmpty(queryId) &&
|
||||
selectedPanelType !== PANEL_TYPES.LIST && { 'X-SIGNOZ-QUERY-ID': queryId }),
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -0,0 +1,112 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Typography } from 'antd';
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ChevronUp,
|
||||
Command,
|
||||
CornerDownLeft,
|
||||
Slash,
|
||||
} from 'lucide-react';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
|
||||
import ExampleQueriesRendererForLogs from '../QueryBuilderSearch/ExampleQueriesRendererForLogs';
|
||||
import { convertExampleQueriesToOptions } from '../QueryBuilderSearch/utils';
|
||||
import { ITag, Option } from './QueryBuilderSearchV2';
|
||||
|
||||
interface ICustomDropdownProps {
|
||||
menu: React.ReactElement;
|
||||
searchValue: string;
|
||||
tags: ITag[];
|
||||
options: Option[];
|
||||
exampleQueries: TagFilter[];
|
||||
onChange: (value: TagFilter) => void;
|
||||
currentFilterItem?: ITag;
|
||||
}
|
||||
|
||||
export default function QueryBuilderSearchDropdown(
|
||||
props: ICustomDropdownProps,
|
||||
): React.ReactElement {
|
||||
const {
|
||||
menu,
|
||||
currentFilterItem,
|
||||
searchValue,
|
||||
tags,
|
||||
exampleQueries,
|
||||
options,
|
||||
onChange,
|
||||
} = props;
|
||||
const userOs = getUserOperatingSystem();
|
||||
return (
|
||||
<>
|
||||
<div className="content">
|
||||
{!currentFilterItem?.key ? (
|
||||
<div className="suggested-filters">Suggested Filters</div>
|
||||
) : !currentFilterItem?.op ? (
|
||||
<div className="operator-for">
|
||||
<Typography.Text className="operator-for-text">
|
||||
Operator for{' '}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="operator-for-value">
|
||||
{currentFilterItem?.key?.key}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="value-for">
|
||||
<Typography.Text className="value-for-text">
|
||||
Value(s) for{' '}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="value-for-value">
|
||||
{currentFilterItem?.key?.key} {currentFilterItem?.op}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
{menu}
|
||||
{!searchValue && tags.length === 0 && (
|
||||
<div className="example-queries">
|
||||
<div className="heading"> Example Queries </div>
|
||||
<div className="query-container">
|
||||
{convertExampleQueriesToOptions(exampleQueries).map((query) => (
|
||||
<ExampleQueriesRendererForLogs
|
||||
key={query.label}
|
||||
label={query.label}
|
||||
value={query.value}
|
||||
handleAddTag={onChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="keyboard-shortcuts">
|
||||
<section className="navigate">
|
||||
<ArrowDown size={10} className="icons" />
|
||||
<ArrowUp size={10} className="icons" />
|
||||
<span className="keyboard-text">to navigate</span>
|
||||
</section>
|
||||
<section className="update-query">
|
||||
<CornerDownLeft size={10} className="icons" />
|
||||
<span className="keyboard-text">to update query</span>
|
||||
</section>
|
||||
{!currentFilterItem?.key && options.length > 3 && (
|
||||
<section className="show-all-filter-items">
|
||||
{userOs === UserOperatingSystem.MACOS ? (
|
||||
<Command size={14} className="icons" />
|
||||
) : (
|
||||
<ChevronUp size={14} className="icons" />
|
||||
)}
|
||||
+
|
||||
<Slash size={14} className="icons" />
|
||||
<span className="keyboard-text">Show all filter items</span>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
QueryBuilderSearchDropdown.defaultProps = {
|
||||
currentFilterItem: undefined,
|
||||
};
|
@ -0,0 +1,261 @@
|
||||
.query-builder-search-v2 {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.show-all-filters {
|
||||
.content {
|
||||
.rc-virtual-list-holder {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.suggested-filters {
|
||||
color: var(--bg-slate-50);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 0px 8px 14px;
|
||||
}
|
||||
|
||||
.operator-for {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 0px 8px 14px;
|
||||
|
||||
.operator-for-text {
|
||||
color: var(--bg-slate-200);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.operator-for-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0px 8px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: 50px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
|
||||
.value-for {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 0px 8px 14px;
|
||||
.value-for-text {
|
||||
color: var(--bg-slate-200);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.value-for-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0px 8px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: 50px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
.example-queries {
|
||||
cursor: default;
|
||||
.heading {
|
||||
padding: 12px 14px 8px 14px;
|
||||
color: var(--bg-slate-50);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.query-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 0px 12px 12px 12px;
|
||||
cursor: pointer;
|
||||
|
||||
.example-query {
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-ink-200);
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: -0.07px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.example-query:hover {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.keyboard-shortcuts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
padding: 11px 16px;
|
||||
cursor: default;
|
||||
|
||||
.icons {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2.286px;
|
||||
border-top: 1.143px solid var(--bg-ink-200);
|
||||
border-right: 1.143px solid var(--bg-ink-200);
|
||||
border-bottom: 2.286px solid var(--bg-ink-200);
|
||||
border-left: 1.143px solid var(--bg-ink-200);
|
||||
background: var(--Ink-400, #121317);
|
||||
}
|
||||
|
||||
.keyboard-text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.navigate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 12px;
|
||||
gap: 4px;
|
||||
border-right: 1px solid #1d212d;
|
||||
}
|
||||
|
||||
.update-query {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
gap: 4px;
|
||||
}
|
||||
.show-all-filter-items {
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid #1d212d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.qb-search-bar-tokenised-tags {
|
||||
.ant-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
background: var(--bg-slate-300);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
padding: 0px;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-100);
|
||||
font-family: Inter;
|
||||
font-size: 14px !important;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.ant-tag-close-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
margin-inline-start: 0px !important;
|
||||
margin-inline-end: 0px !important;
|
||||
}
|
||||
|
||||
&.resource {
|
||||
border: 1px solid rgba(242, 71, 105, 0.2);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-sakura-400);
|
||||
background: rgba(245, 108, 135, 0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ant-tag-close-icon {
|
||||
background: rgba(245, 108, 135, 0.1);
|
||||
}
|
||||
}
|
||||
&.tag {
|
||||
border: 1px solid rgba(189, 153, 121, 0.2);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-sienna-400);
|
||||
background: rgba(189, 153, 121, 0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ant-tag-close-icon {
|
||||
background: rgba(189, 153, 121, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,862 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './QueryBuilderSearchV2.styles.scss';
|
||||
|
||||
import { Select, Spin, Tag, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
OPERATORS,
|
||||
QUERY_BUILDER_OPERATORS_BY_TYPES,
|
||||
QUERY_BUILDER_SEARCH_VALUES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
|
||||
import { validationMapper } from 'hooks/queryBuilder/useIsValidTag';
|
||||
import { operatorTypeMapper } from 'hooks/queryBuilder/useOperatorType';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useDebounceValue from 'hooks/useDebounce';
|
||||
import {
|
||||
cloneDeep,
|
||||
isArray,
|
||||
isEmpty,
|
||||
isEqual,
|
||||
isObject,
|
||||
isUndefined,
|
||||
unset,
|
||||
} from 'lodash-es';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { BaseSelectRef } from 'rc-select';
|
||||
import {
|
||||
KeyboardEvent,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
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 QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
|
||||
import Suggestions from './Suggestions';
|
||||
|
||||
export interface ITag {
|
||||
id?: string;
|
||||
key: BaseAutocompleteData;
|
||||
op: string;
|
||||
value: string[] | string | number | boolean;
|
||||
}
|
||||
|
||||
interface CustomTagProps {
|
||||
label: React.ReactNode;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
onClose: () => void;
|
||||
closable: boolean;
|
||||
}
|
||||
|
||||
interface QueryBuilderSearchV2Props {
|
||||
query: IBuilderQuery;
|
||||
onChange: (value: TagFilter) => void;
|
||||
whereClauseConfig?: WhereClauseConfig;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
suffixIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
label: string;
|
||||
value: BaseAutocompleteData | string;
|
||||
}
|
||||
|
||||
export enum DropdownState {
|
||||
ATTRIBUTE_KEY = 'ATTRIBUTE_KEY',
|
||||
OPERATOR = 'OPERATOR',
|
||||
ATTRIBUTE_VALUE = 'ATTRIBUTE_VALUE',
|
||||
}
|
||||
|
||||
function getInitTags(query: IBuilderQuery): ITag[] {
|
||||
return query.filters.items.map((item) => ({
|
||||
id: item.id,
|
||||
key: item.key as BaseAutocompleteData,
|
||||
op: item.op,
|
||||
value: `${item.value}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function QueryBuilderSearchV2(
|
||||
props: QueryBuilderSearchV2Props,
|
||||
): React.ReactElement {
|
||||
const {
|
||||
query,
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
suffixIcon,
|
||||
whereClauseConfig,
|
||||
} = props;
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const { handleRunQuery, currentQuery } = useQueryBuilder();
|
||||
|
||||
const selectRef = useRef<BaseSelectRef>(null);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
// create the tags from the initial query here, this should only be computed on the first load as post that tags and query will be always in sync.
|
||||
const [tags, setTags] = useState<ITag[]>(() => getInitTags(query));
|
||||
|
||||
// this will maintain the current state of in process filter item
|
||||
const [currentFilterItem, setCurrentFilterItem] = useState<ITag | undefined>();
|
||||
|
||||
const [currentState, setCurrentState] = useState<DropdownState>(
|
||||
DropdownState.ATTRIBUTE_KEY,
|
||||
);
|
||||
|
||||
// to maintain the current running state until the tokenization happens for the tag
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
|
||||
const [dropdownOptions, setDropdownOptions] = useState<Option[]>([]);
|
||||
|
||||
const [showAllFilters, setShowAllFilters] = useState<boolean>(false);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const memoizedSearchParams = useMemo(
|
||||
() => [
|
||||
searchValue,
|
||||
query.dataSource,
|
||||
query.aggregateOperator,
|
||||
query.aggregateAttribute.key,
|
||||
],
|
||||
[
|
||||
searchValue,
|
||||
query.dataSource,
|
||||
query.aggregateOperator,
|
||||
query.aggregateAttribute.key,
|
||||
],
|
||||
);
|
||||
|
||||
const queryFiltersWithoutId = useMemo(
|
||||
() => ({
|
||||
...query.filters,
|
||||
items: query.filters.items.map((item) => {
|
||||
const filterWithoutId = cloneDeep(item);
|
||||
unset(filterWithoutId, 'id');
|
||||
return filterWithoutId;
|
||||
}),
|
||||
}),
|
||||
[query.filters],
|
||||
);
|
||||
|
||||
const memoizedSuggestionsParams = useMemo(
|
||||
() => [searchValue, query.dataSource, queryFiltersWithoutId],
|
||||
[query.dataSource, queryFiltersWithoutId, searchValue],
|
||||
);
|
||||
|
||||
const memoizedValueParams = useMemo(
|
||||
() => [
|
||||
query.aggregateOperator,
|
||||
query.dataSource,
|
||||
query.aggregateAttribute.key,
|
||||
currentFilterItem?.key?.key || '',
|
||||
currentFilterItem?.key?.dataType,
|
||||
currentFilterItem?.key?.type ?? '',
|
||||
isArray(currentFilterItem?.value)
|
||||
? currentFilterItem?.value?.[currentFilterItem.value.length - 1]
|
||||
: currentFilterItem?.value,
|
||||
],
|
||||
[
|
||||
query.aggregateOperator,
|
||||
query.dataSource,
|
||||
query.aggregateAttribute.key,
|
||||
currentFilterItem?.key?.key,
|
||||
currentFilterItem?.key?.dataType,
|
||||
currentFilterItem?.key?.type,
|
||||
currentFilterItem?.value,
|
||||
],
|
||||
);
|
||||
|
||||
const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY);
|
||||
|
||||
const valueParams = useDebounceValue(memoizedValueParams, DEBOUNCE_DELAY);
|
||||
|
||||
const suggestionsParams = useDebounceValue(
|
||||
memoizedSuggestionsParams,
|
||||
DEBOUNCE_DELAY,
|
||||
);
|
||||
|
||||
const isQueryEnabled = useMemo(() => {
|
||||
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||
return query.dataSource === DataSource.METRICS
|
||||
? !!query.aggregateOperator &&
|
||||
!!query.dataSource &&
|
||||
!!query.aggregateAttribute.dataType
|
||||
: true;
|
||||
}
|
||||
return false;
|
||||
}, [
|
||||
currentState,
|
||||
query.aggregateAttribute.dataType,
|
||||
query.aggregateOperator,
|
||||
query.dataSource,
|
||||
]);
|
||||
|
||||
const { data, isFetching } = useGetAggregateKeys(
|
||||
{
|
||||
searchText: searchValue,
|
||||
dataSource: query.dataSource,
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
aggregateAttribute: query.aggregateAttribute.key,
|
||||
tagType: query.aggregateAttribute.type ?? null,
|
||||
},
|
||||
{
|
||||
queryKey: [searchParams],
|
||||
enabled: isQueryEnabled && !isLogsExplorerPage,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: suggestionsData,
|
||||
isFetching: isFetchingSuggestions,
|
||||
} = useGetAttributeSuggestions(
|
||||
{
|
||||
searchText: searchValue.split(' ')[0],
|
||||
dataSource: query.dataSource,
|
||||
filters: query.filters,
|
||||
},
|
||||
{
|
||||
queryKey: [suggestionsParams],
|
||||
enabled: isQueryEnabled && isLogsExplorerPage,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: attributeValues,
|
||||
isFetching: isFetchingAttributeValues,
|
||||
} = useGetAggregateValues(
|
||||
{
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
dataSource: query.dataSource,
|
||||
aggregateAttribute: query.aggregateAttribute.key,
|
||||
attributeKey: currentFilterItem?.key?.key || '',
|
||||
filterAttributeKeyDataType:
|
||||
currentFilterItem?.key?.dataType ?? DataTypes.EMPTY,
|
||||
tagType: currentFilterItem?.key?.type ?? '',
|
||||
searchText: isArray(currentFilterItem?.value)
|
||||
? currentFilterItem?.value?.[currentFilterItem.value.length - 1] || ''
|
||||
: currentFilterItem?.value?.toString() || '',
|
||||
},
|
||||
{
|
||||
enabled: currentState === DropdownState.ATTRIBUTE_VALUE,
|
||||
queryKey: [valueParams],
|
||||
},
|
||||
);
|
||||
|
||||
const handleDropdownSelect = useCallback(
|
||||
(value: string) => {
|
||||
let parsedValue: BaseAutocompleteData | string;
|
||||
|
||||
try {
|
||||
parsedValue = JSON.parse(value);
|
||||
} catch {
|
||||
parsedValue = value;
|
||||
}
|
||||
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
...prev,
|
||||
key: parsedValue as BaseAutocompleteData,
|
||||
op: '',
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
|
||||
} else if (currentState === DropdownState.OPERATOR) {
|
||||
if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: value,
|
||||
value: '',
|
||||
} as ITag,
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: value as string,
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
setSearchValue(`${currentFilterItem?.key?.key} ${value}`);
|
||||
}
|
||||
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||
const operatorType =
|
||||
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
|
||||
const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY;
|
||||
|
||||
if (isMulti) {
|
||||
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
|
||||
const newSearch = [...tagValue];
|
||||
newSearch[newSearch.length === 0 ? 0 : newSearch.length - 1] = value;
|
||||
const newSearchValue = newSearch.join(',');
|
||||
setSearchValue(`${tagKey} ${tagOperator} ${newSearchValue},`);
|
||||
} else {
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
setCurrentFilterItem(undefined);
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: currentFilterItem?.op,
|
||||
value,
|
||||
} as ITag,
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
[currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback((value: string) => {
|
||||
setSearchValue(value);
|
||||
}, []);
|
||||
|
||||
const onInputKeyDownHandler = useCallback(
|
||||
(event: KeyboardEvent<Element>): void => {
|
||||
if (event.key === 'Backspace' && !searchValue) {
|
||||
event.stopPropagation();
|
||||
setTags((prev) => prev.slice(0, -1));
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === '/') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setShowAllFilters((prev) => !prev);
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleRunQuery();
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
[handleRunQuery, searchValue],
|
||||
);
|
||||
|
||||
const handleOnBlur = useCallback((): void => {
|
||||
if (searchValue) {
|
||||
const operatorType =
|
||||
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
|
||||
if (
|
||||
currentFilterItem?.key &&
|
||||
isEmpty(currentFilterItem?.op) &&
|
||||
whereClauseConfig?.customKey === 'body' &&
|
||||
whereClauseConfig?.customOp === OPERATORS.CONTAINS
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: {
|
||||
key: 'body',
|
||||
dataType: DataTypes.String,
|
||||
type: '',
|
||||
isColumn: true,
|
||||
isJSON: false,
|
||||
id: 'body--string----true',
|
||||
},
|
||||
op: OPERATORS.CONTAINS,
|
||||
value: currentFilterItem?.key?.key,
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else if (
|
||||
currentFilterItem?.op === OPERATORS.EXISTS ||
|
||||
currentFilterItem?.op === OPERATORS.NOT_EXISTS
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: currentFilterItem?.op,
|
||||
value: '',
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else if (
|
||||
validationMapper[operatorType]?.(
|
||||
isArray(currentFilterItem?.value)
|
||||
? currentFilterItem?.value.length || 0
|
||||
: 1,
|
||||
)
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key as BaseAutocompleteData,
|
||||
op: currentFilterItem?.op as string,
|
||||
value: currentFilterItem?.value || '',
|
||||
},
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentFilterItem?.key,
|
||||
currentFilterItem?.op,
|
||||
currentFilterItem?.value,
|
||||
searchValue,
|
||||
whereClauseConfig?.customKey,
|
||||
whereClauseConfig?.customOp,
|
||||
]);
|
||||
|
||||
// this useEffect takes care of tokenisation based on the search state
|
||||
useEffect(() => {
|
||||
if (isFetchingSuggestions) {
|
||||
return;
|
||||
}
|
||||
if (!searchValue) {
|
||||
setCurrentFilterItem(undefined);
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
}
|
||||
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
|
||||
|
||||
if (tagKey && isUndefined(currentFilterItem?.key)) {
|
||||
let currentRunningAttributeKey;
|
||||
const isSuggestedKeyInAutocomplete = suggestionsData?.payload?.attributes?.some(
|
||||
(value) => value.key === tagKey.split(' ')[0],
|
||||
);
|
||||
|
||||
if (isSuggestedKeyInAutocomplete) {
|
||||
const allAttributesMatchingTheKey =
|
||||
suggestionsData?.payload?.attributes?.filter(
|
||||
(value) => value.key === tagKey.split(' ')[0],
|
||||
) || [];
|
||||
|
||||
if (allAttributesMatchingTheKey?.length === 1) {
|
||||
[currentRunningAttributeKey] = allAttributesMatchingTheKey;
|
||||
}
|
||||
if (allAttributesMatchingTheKey?.length > 1) {
|
||||
// the priority logic goes here
|
||||
[currentRunningAttributeKey] = allAttributesMatchingTheKey;
|
||||
}
|
||||
|
||||
if (currentRunningAttributeKey) {
|
||||
setCurrentFilterItem({
|
||||
key: currentRunningAttributeKey,
|
||||
op: '',
|
||||
value: '',
|
||||
});
|
||||
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
}
|
||||
}
|
||||
if (suggestionsData?.payload?.attributes?.length === 0) {
|
||||
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,
|
||||
isJSON: false,
|
||||
},
|
||||
op: '',
|
||||
value: '',
|
||||
});
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
}
|
||||
} else if (
|
||||
currentFilterItem?.key &&
|
||||
currentFilterItem?.key?.key !== tagKey.split(' ')[0]
|
||||
) {
|
||||
setCurrentFilterItem(undefined);
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else if (tagOperator && isEmpty(currentFilterItem?.op)) {
|
||||
if (
|
||||
tagOperator === OPERATORS.EXISTS ||
|
||||
tagOperator === OPERATORS.NOT_EXISTS
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
{
|
||||
key: currentFilterItem?.key,
|
||||
op: tagOperator,
|
||||
value: '',
|
||||
} as ITag,
|
||||
]);
|
||||
setCurrentFilterItem(undefined);
|
||||
setSearchValue('');
|
||||
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||
} else {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: tagOperator,
|
||||
value: '',
|
||||
}));
|
||||
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
}
|
||||
} else if (
|
||||
!isEmpty(currentFilterItem?.op) &&
|
||||
tagOperator !== currentFilterItem?.op
|
||||
) {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: '',
|
||||
value: '',
|
||||
}));
|
||||
setCurrentState(DropdownState.OPERATOR);
|
||||
} else if (!isEmpty(tagValue)) {
|
||||
const currentValue = {
|
||||
key: currentFilterItem?.key as BaseAutocompleteData,
|
||||
operator: currentFilterItem?.op as string,
|
||||
value: tagValue,
|
||||
};
|
||||
if (!isEqual(currentValue, currentFilterItem)) {
|
||||
setCurrentFilterItem((prev) => ({
|
||||
key: prev?.key as BaseAutocompleteData,
|
||||
op: prev?.op as string,
|
||||
value: tagValue,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentFilterItem,
|
||||
currentFilterItem?.key,
|
||||
currentFilterItem?.op,
|
||||
suggestionsData?.payload?.attributes,
|
||||
searchValue,
|
||||
isFetchingSuggestions,
|
||||
]);
|
||||
|
||||
// the useEffect takes care of setting the dropdown values correctly on change of the current state
|
||||
useEffect(() => {
|
||||
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||
if (isLogsExplorerPage) {
|
||||
setDropdownOptions(
|
||||
suggestionsData?.payload?.attributes?.map((key) => ({
|
||||
label: key.key,
|
||||
value: key,
|
||||
})) || [],
|
||||
);
|
||||
} else {
|
||||
setDropdownOptions(
|
||||
data?.payload?.attributeKeys?.map((key) => ({
|
||||
label: key.key,
|
||||
value: key,
|
||||
})) || [],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (currentState === DropdownState.OPERATOR) {
|
||||
const keyOperator = searchValue.split(' ');
|
||||
const partialOperator = keyOperator?.[1];
|
||||
const strippedKey = keyOperator?.[0];
|
||||
|
||||
let operatorOptions;
|
||||
if (currentFilterItem?.key?.dataType) {
|
||||
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES[
|
||||
currentFilterItem.key
|
||||
.dataType as keyof typeof QUERY_BUILDER_OPERATORS_BY_TYPES
|
||||
].map((operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}));
|
||||
|
||||
if (partialOperator) {
|
||||
operatorOptions = operatorOptions.filter((op) =>
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
|
||||
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}));
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else {
|
||||
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
|
||||
(operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}),
|
||||
);
|
||||
|
||||
if (partialOperator) {
|
||||
operatorOptions = operatorOptions.filter((op) =>
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
setDropdownOptions(operatorOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||
const values: string[] =
|
||||
Object.values(attributeValues?.payload || {}).find((el) => !!el) || [];
|
||||
|
||||
const { tagValue } = getTagToken(searchValue);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
setDropdownOptions(
|
||||
values.map((val) => ({
|
||||
label: val,
|
||||
value: val,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}, [
|
||||
attributeValues?.payload,
|
||||
currentFilterItem?.key.dataType,
|
||||
currentState,
|
||||
data?.payload?.attributeKeys,
|
||||
isLogsExplorerPage,
|
||||
searchValue,
|
||||
suggestionsData?.payload?.attributes,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const filterTags: IBuilderQuery['filters'] = {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
};
|
||||
tags.forEach((tag) => {
|
||||
filterTags.items.push({
|
||||
id: tag.id || uuid().slice(0, 8),
|
||||
key: tag.key,
|
||||
op: tag.op,
|
||||
value: tag.value,
|
||||
});
|
||||
});
|
||||
|
||||
if (!isEqual(query.filters, filterTags)) {
|
||||
onChange(filterTags);
|
||||
setTags(filterTags.items as ITag[]);
|
||||
}
|
||||
}, [onChange, query.filters, tags]);
|
||||
|
||||
const isLastQuery = useMemo(
|
||||
() =>
|
||||
isEqual(
|
||||
currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1],
|
||||
query,
|
||||
),
|
||||
[currentQuery, query],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLastQuery) {
|
||||
registerShortcut(LogsExplorerShortcuts.FocusTheSearchBar, () => {
|
||||
// set timeout is needed here else the select treats the hotkey as input value
|
||||
setTimeout(() => {
|
||||
selectRef.current?.focus();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
return (): void =>
|
||||
deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar);
|
||||
}, [deregisterShortcut, isLastQuery, registerShortcut]);
|
||||
|
||||
const loading = useMemo(
|
||||
() => isFetching || isFetchingAttributeValues || isFetchingSuggestions,
|
||||
[isFetching, isFetchingAttributeValues, isFetchingSuggestions],
|
||||
);
|
||||
|
||||
const isMetricsDataSource = useMemo(
|
||||
() => query.dataSource === DataSource.METRICS,
|
||||
[query.dataSource],
|
||||
);
|
||||
|
||||
const queryTags = useMemo(
|
||||
() => tags.map((tag) => `${tag.key.key} ${tag.op} ${tag.value}`),
|
||||
[tags],
|
||||
);
|
||||
|
||||
const onTagRender = ({
|
||||
value,
|
||||
closable,
|
||||
onClose,
|
||||
}: CustomTagProps): React.ReactElement => {
|
||||
const { tagOperator } = getTagToken(value);
|
||||
const isInNin = isInNInOperator(tagOperator);
|
||||
const chipValue = isInNin
|
||||
? value?.trim()?.replace(/,\s*$/, '')
|
||||
: value?.trim();
|
||||
|
||||
const indexInQueryTags = queryTags.findIndex((qTag) => isEqual(qTag, value));
|
||||
const tagDetails = tags[indexInQueryTags];
|
||||
|
||||
const onCloseHandler = (): void => {
|
||||
onClose();
|
||||
setSearchValue('');
|
||||
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
|
||||
};
|
||||
|
||||
const tagEditHandler = (value: string): void => {
|
||||
setCurrentFilterItem(tagDetails);
|
||||
setSearchValue(value);
|
||||
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
|
||||
};
|
||||
|
||||
const isDisabled = !!searchValue;
|
||||
|
||||
return (
|
||||
<span className="qb-search-bar-tokenised-tags">
|
||||
<Tag
|
||||
closable={!searchValue && closable}
|
||||
onClose={onCloseHandler}
|
||||
className={tagDetails?.key?.type || ''}
|
||||
>
|
||||
<Tooltip title={chipValue}>
|
||||
<TypographyText
|
||||
ellipsis
|
||||
$isInNin={isInNin}
|
||||
disabled={isDisabled}
|
||||
$isEnabled={!!searchValue}
|
||||
onClick={(): void => {
|
||||
if (!isDisabled) tagEditHandler(value);
|
||||
}}
|
||||
>
|
||||
{chipValue}
|
||||
</TypographyText>
|
||||
</Tooltip>
|
||||
</Tag>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="query-builder-search-v2">
|
||||
<Select
|
||||
ref={selectRef}
|
||||
getPopupContainer={popupContainer}
|
||||
virtual={false}
|
||||
showSearch
|
||||
tagRender={onTagRender}
|
||||
transitionName=""
|
||||
choiceTransitionName=""
|
||||
filterOption={false}
|
||||
open={isOpen}
|
||||
suffixIcon={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
!isUndefined(suffixIcon) ? (
|
||||
suffixIcon
|
||||
) : isOpen ? (
|
||||
<ChevronUp size={14} />
|
||||
) : (
|
||||
<ChevronDown size={14} />
|
||||
)
|
||||
}
|
||||
onDropdownVisibleChange={setIsOpen}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
placeholder={placeholder}
|
||||
value={queryTags}
|
||||
searchValue={searchValue}
|
||||
className={cx(
|
||||
!currentFilterItem?.key && !showAllFilters && dropdownOptions.length > 3
|
||||
? 'show-all-filters'
|
||||
: '',
|
||||
className,
|
||||
)}
|
||||
rootClassName="query-builder-search"
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||
style={selectStyle}
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleDropdownSelect}
|
||||
onInputKeyDown={onInputKeyDownHandler}
|
||||
notFoundContent={loading ? <Spin size="small" /> : null}
|
||||
showAction={['focus']}
|
||||
onBlur={handleOnBlur}
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
dropdownRender={(menu): ReactElement => (
|
||||
<QueryBuilderSearchDropdown
|
||||
menu={menu}
|
||||
options={dropdownOptions}
|
||||
onChange={(val: TagFilter): void => {
|
||||
setTags((prev) => [...prev, ...(val.items as ITag[])]);
|
||||
}}
|
||||
searchValue={searchValue}
|
||||
exampleQueries={suggestionsData?.payload?.example_queries || []}
|
||||
tags={tags}
|
||||
currentFilterItem={currentFilterItem}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{dropdownOptions.map((option) => {
|
||||
let val = option.value;
|
||||
try {
|
||||
if (isObject(option.value)) {
|
||||
val = JSON.stringify(option.value);
|
||||
} else {
|
||||
val = option.value;
|
||||
}
|
||||
} catch {
|
||||
val = option.value;
|
||||
}
|
||||
return (
|
||||
<Select.Option key={isObject(val) ? `select-option` : val} value={val}>
|
||||
<Suggestions
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
option={currentState}
|
||||
/>
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
QueryBuilderSearchV2.defaultProps = {
|
||||
placeholder: PLACEHOLDER,
|
||||
className: '',
|
||||
suffixIcon: null,
|
||||
whereClauseConfig: {},
|
||||
};
|
||||
|
||||
export default QueryBuilderSearchV2;
|
@ -0,0 +1,147 @@
|
||||
.text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.option {
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
gap: 8px;
|
||||
|
||||
.value {
|
||||
}
|
||||
}
|
||||
.right-section {
|
||||
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);
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0px 6px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: 50px;
|
||||
text-transform: capitalize;
|
||||
|
||||
&.tag {
|
||||
border-radius: 50px;
|
||||
background: rgba(189, 153, 121, 0.1) !important;
|
||||
color: var(--bg-sienna-400) !important;
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-sienna-400);
|
||||
}
|
||||
.text {
|
||||
color: var(--bg-sienna-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
|
||||
&.resource {
|
||||
border-radius: 50px;
|
||||
background: rgba(245, 108, 135, 0.1) !important;
|
||||
color: var(--bg-sakura-400) !important;
|
||||
|
||||
.dot {
|
||||
background-color: var(--bg-sakura-400);
|
||||
}
|
||||
.text {
|
||||
color: var(--bg-sakura-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.option-meta-data-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-without-tag {
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.option:hover {
|
||||
.container {
|
||||
.left-section {
|
||||
.value {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
.container-without-tag {
|
||||
.value {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import './Suggestions.styles.scss';
|
||||
|
||||
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 { useMemo, useState } from 'react';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { DropdownState } from './QueryBuilderSearchV2';
|
||||
|
||||
interface ISuggestionsProps {
|
||||
label: string;
|
||||
value: BaseAutocompleteData | string;
|
||||
option: DropdownState;
|
||||
}
|
||||
|
||||
function Suggestions(props: ISuggestionsProps): React.ReactElement {
|
||||
const { label, value, option } = props;
|
||||
|
||||
const optionType = useMemo(() => {
|
||||
if (isObject(value)) {
|
||||
return value.type;
|
||||
}
|
||||
return '';
|
||||
}, [value]);
|
||||
|
||||
const [truncated, setTruncated] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div className="option">
|
||||
{!isEmpty(optionType) && isObject(value) ? (
|
||||
<Tooltip title={truncated ? `${value.key}` : ''} placement="topLeft">
|
||||
<div className="container">
|
||||
<section className="left-section">
|
||||
{value.isIndexed ? (
|
||||
<Zap size={12} fill={Color.BG_AMBER_500} />
|
||||
) : (
|
||||
<div className="dot" />
|
||||
)}
|
||||
<Typography.Text
|
||||
className="text value"
|
||||
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
|
||||
>
|
||||
{label}
|
||||
</Typography.Text>
|
||||
</section>
|
||||
<section className="right-section">
|
||||
<Typography.Text className="data-type">{value.dataType}</Typography.Text>
|
||||
<section className={cx('type-tag', value.type)}>
|
||||
<div className="dot" />
|
||||
<Typography.Text className="text">{value.type}</Typography.Text>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<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>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Suggestions;
|
33
frontend/src/hooks/queryBuilder/useGetAggregateValues.ts
Normal file
33
frontend/src/hooks/queryBuilder/useGetAggregateValues.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
IAttributeValuesResponse,
|
||||
IGetAttributeValuesPayload,
|
||||
} from 'types/api/queryBuilder/getAttributesValues';
|
||||
|
||||
type UseGetAttributeValues = (
|
||||
requestData: IGetAttributeValuesPayload,
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
|
||||
>,
|
||||
) => UseQueryResult<SuccessResponse<IAttributeValuesResponse> | ErrorResponse>;
|
||||
|
||||
export const useGetAggregateValues: UseGetAttributeValues = (
|
||||
requestData,
|
||||
options,
|
||||
) => {
|
||||
const queryKey = useMemo(() => {
|
||||
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||
return [...options.queryKey];
|
||||
}
|
||||
return [requestData];
|
||||
}, [options?.queryKey, requestData]);
|
||||
|
||||
return useQuery<SuccessResponse<IAttributeValuesResponse> | ErrorResponse>({
|
||||
queryKey,
|
||||
queryFn: () => getAttributesValues(requestData),
|
||||
...options,
|
||||
});
|
||||
};
|
@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
|
||||
import { OperatorType } from './useOperatorType';
|
||||
|
||||
const validationMapper: Record<
|
||||
export const validationMapper: Record<
|
||||
OperatorType,
|
||||
(resultLength: number) => boolean
|
||||
> = {
|
||||
|
@ -6,7 +6,7 @@ export type OperatorType =
|
||||
| 'NON_VALUE'
|
||||
| 'NOT_VALID';
|
||||
|
||||
const operatorTypeMapper: Record<string, OperatorType> = {
|
||||
export const operatorTypeMapper: Record<string, OperatorType> = {
|
||||
[OPERATORS.IN]: 'MULTIPLY_VALUE',
|
||||
[OPERATORS.NIN]: 'MULTIPLY_VALUE',
|
||||
[OPERATORS.EXISTS]: 'NON_VALUE',
|
||||
|
Loading…
x
Reference in New Issue
Block a user