mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 20:59:02 +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,
|
undefined,
|
||||||
listQueryKeyRef,
|
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';
|
import { OperatorType } from './useOperatorType';
|
||||||
|
|
||||||
const validationMapper: Record<
|
export const validationMapper: Record<
|
||||||
OperatorType,
|
OperatorType,
|
||||||
(resultLength: number) => boolean
|
(resultLength: number) => boolean
|
||||||
> = {
|
> = {
|
||||||
|
@ -6,7 +6,7 @@ export type OperatorType =
|
|||||||
| 'NON_VALUE'
|
| 'NON_VALUE'
|
||||||
| 'NOT_VALID';
|
| 'NOT_VALID';
|
||||||
|
|
||||||
const operatorTypeMapper: Record<string, OperatorType> = {
|
export const operatorTypeMapper: Record<string, OperatorType> = {
|
||||||
[OPERATORS.IN]: 'MULTIPLY_VALUE',
|
[OPERATORS.IN]: 'MULTIPLY_VALUE',
|
||||||
[OPERATORS.NIN]: 'MULTIPLY_VALUE',
|
[OPERATORS.NIN]: 'MULTIPLY_VALUE',
|
||||||
[OPERATORS.EXISTS]: 'NON_VALUE',
|
[OPERATORS.EXISTS]: 'NON_VALUE',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user