fix: issues on WHERE search filter (#2629)

* fix: search filter validation on data source

* fix: value search not working for in/nin

* fix: unwanted key api while searching value & disabled tag

* fix: unnecessary , at end of in/nin value

* fix: added space after operator to get value

* fix: custom value not being selected

* fix: space after tag and value

* fix: api call debounce duration

* fix: suggested changes

* fix: updated query params

* fix: search filter data for logs and traces

* fix: search filter value data type issue

* fix: search filter value tag type

* fix: chip & iscolumn issue

* fix: null handled

* fix: label in list of search filter component

* fix: label in list of search filter component

* fix: code level changes

* fix: incorrect filter operators

* fix: key selection dancing

* fix: missing suggestion

* fix: keys are not getting updated

* fix: strange behaviour - removed duplicate options

* fix: driver id exclusion not working

* fix: loader when 0 options

* fix: exists/not-exists tag value issue

* fix: some weird behaviour about exists

* fix: added duplicate option remove logic at hook level

* fix: removed empty options from list

* fix: closable chip handler on edit

* fix: search filter validation on data source

* fix: value search not working for in/nin

* fix: unwanted key api while searching value & disabled tag

* fix: unnecessary , at end of in/nin value

* fix: added space after operator to get value

* fix: custom value not being selected

* fix: space after tag and value

* fix: api call debounce duration

* fix: suggested changes

* fix: updated query params

* fix: search filter data for logs and traces

* fix: search filter value data type issue

* fix: search filter value tag type

* fix: chip & iscolumn issue

* fix: null handled

* fix: label in list of search filter component

* fix: label in list of search filter component

* fix: code level changes

* fix: incorrect filter operators

* fix: key selection dancing

* fix: missing suggestion

* fix: keys are not getting updated

* fix: strange behaviour - removed duplicate options

* fix: driver id exclusion not working

* fix: loader when 0 options

* fix: exists/not-exists tag value issue

* fix: some weird behaviour about exists

* fix: added duplicate option remove logic at hook level

* fix: removed empty options from list

* fix: closable chip handler on edit

* fix: search filter validation on data source

* fix: lint issues is fixed

* fix: chip & iscolumn issue

* fix: lint changes are updated

* fix: undefined case handled

* fix: undefined case handled

* fix: removed settimeout

* fix: delete chip getting value undefined

* fix: payload correctness

* fix: incorrect value selection

* fix: key text typing doesn't change anything

* fix: search value issue

* fix: payload updated

* fix: auto populate value issue

* fix: payload updated & populate values

* fix: split value for in/nin

* fix: split value getting undefined

* fix: new version of search filter using papaparse library

* fix: removed unwanted space before operator

* fix: added exact find method & removed includes logic

* fix: issue when user create chip for exists not exists operator

* fix: white space logic removed

* fix: allow custom key in from list

* fix: issue when user create chip for exists not exists operator

* fix: removed unwanted includes

* fix: removed unwanted utils function

* fix: replaced join with papa unparse

* fix: removed get count of space utils

* fix: resolved build issue

* fix: code level fixes

* fix: space after key

* fix: quote a value if comma present

* fix: handle custom key object onchange

* chore: coverted into string

* Merge branch 'develop' into fix/issue-search-filter

* chore: eslint rule disabling is removed

* fix: serviceName contains sql

* chore: less restrictive expression

* fix: custom key selection issue

* chore: papa parse version is made exact

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
Chintan Sudani 2023-05-12 12:30:00 +05:30 committed by GitHub
parent 10e47b5bff
commit 76331001b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 491 additions and 261 deletions

View File

@ -69,6 +69,7 @@
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"mini-css-extract-plugin": "2.4.5",
"papaparse": "5.4.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-force-graph": "^1.41.0",
@ -136,6 +137,7 @@
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/papaparse": "5.3.7",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",

View File

@ -1,6 +1,7 @@
import { ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios';
import createQueryParams from 'lib/createQueryParams';
// ** Helpers
import { ErrorResponse, SuccessResponse } from 'types/api';
// ** Types
@ -18,7 +19,11 @@ export const getAggregateAttribute = async ({
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get(
`autocomplete/aggregate_attributes?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&searchText=${searchText}`,
`autocomplete/aggregate_attributes?${createQueryParams({
aggregateOperator,
searchText,
dataSource,
})}`,
);
return {

View File

@ -1,6 +1,7 @@
import { ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
// ** Types
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
@ -11,6 +12,7 @@ export const getAggregateKeys = async ({
searchText,
dataSource,
aggregateAttribute,
tagType,
}: IGetAttributeKeysPayload): Promise<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
> => {
@ -18,7 +20,12 @@ export const getAggregateKeys = async ({
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get(
`autocomplete/attribute_keys?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&aggregateAttribute=${aggregateAttribute}&searchText=${searchText}`,
`autocomplete/attribute_keys?${createQueryParams({
aggregateOperator,
searchText,
dataSource,
aggregateAttribute,
})}&tagType=${tagType}`,
);
return {

View File

@ -1,63 +0,0 @@
import { ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
export type TagKeyValueProps = {
dataSource: string;
aggregateOperator?: string;
aggregateAttribute?: string;
searchText?: string;
attributeKey?: string;
};
export interface AttributeKeyOptions {
key: string;
type: string;
dataType: 'string' | 'boolean' | 'number';
isColumn: boolean;
}
export const getAttributesKeys = async (
props: TagKeyValueProps,
): Promise<SuccessResponse<AttributeKeyOptions[]> | ErrorResponse> => {
try {
const response = await ApiV3Instance.get(
`/autocomplete/attribute_keys?aggregateOperator=${props.aggregateOperator}&dataSource=${props.dataSource}&aggregateAttribute=${props.aggregateAttribute}&searchText=${props.searchText}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data.attributeKeys,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export interface TagValuePayloadProps {
boolAttributeValues: null | string[];
numberAttributeValues: null | string[];
stringAttributeValues: null | string[];
}
export const getAttributesValues = async (
props: TagKeyValueProps,
): Promise<SuccessResponse<TagValuePayloadProps> | ErrorResponse> => {
try {
const response = await ApiV3Instance.get(
`/autocomplete/attribute_values?aggregateOperator=${props.aggregateOperator}&dataSource=${props.dataSource}&aggregateAttribute=${props.aggregateAttribute}&searchText=${props.searchText}&attributeKey=${props.attributeKey}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@ -0,0 +1,42 @@
import { ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IAttributeValuesResponse,
IGetAttributeValuesPayload,
} from 'types/api/queryBuilder/getAttributesValues';
export const getAttributesValues = async ({
aggregateOperator,
dataSource,
aggregateAttribute,
attributeKey,
filterAttributeKeyDataType,
tagType,
searchText,
}: IGetAttributeValuesPayload): Promise<
SuccessResponse<IAttributeValuesResponse> | ErrorResponse
> => {
try {
const response = await ApiV3Instance.get(
`/autocomplete/attribute_values?${createQueryParams({
aggregateOperator,
dataSource,
aggregateAttribute,
attributeKey,
searchText,
})}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@ -34,6 +34,7 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
export enum QueryBuilderKeys {
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
GET_ATTRIBUTE_KEY = 'GET_ATTRIBUTE_KEY',
}
export const mapOfOperators: Record<DataSource, string[]> = {
@ -88,7 +89,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
aggregateOperator: MetricAggregateOperator.NOOP,
aggregateAttribute: initialAggregateAttribute,
tagFilters: { items: [], op: 'AND' },
filters: { items: [], op: 'AND' },
expression: createNewBuilderItemName({
existNames: [],
sourceNames: alphabet,
@ -166,7 +167,7 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
OPERATORS.EXISTS,
OPERATORS.NOT_EXISTS,
],
number: [
int64: [
OPERATORS['='],
OPERATORS['!='],
OPERATORS.IN,
@ -178,7 +179,19 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
OPERATORS['<='],
OPERATORS['<'],
],
boolean: [
float64: [
OPERATORS['='],
OPERATORS['!='],
OPERATORS.IN,
OPERATORS.NIN,
OPERATORS.EXISTS,
OPERATORS.NOT_EXISTS,
OPERATORS['>='],
OPERATORS['>'],
OPERATORS['<='],
OPERATORS['<'],
],
bool: [
OPERATORS['='],
OPERATORS['!='],
OPERATORS.EXISTS,

View File

@ -25,7 +25,7 @@ export const getQueryBuilderQueries = ({
aggregateAttribute: metricName,
legend,
reduceTo: 'sum',
tagFilters: {
filters: {
items: itemsA,
op: 'AND',
},
@ -60,7 +60,7 @@ export const getQueryBuilderQuerieswithFormula = ({
legend,
aggregateAttribute: metricNameA,
reduceTo: 'sum',
tagFilters: {
filters: {
items: additionalItemsA,
op: 'AND',
},
@ -75,7 +75,7 @@ export const getQueryBuilderQuerieswithFormula = ({
queryName: 'B',
expression: 'B',
reduceTo: 'sum',
tagFilters: {
filters: {
items: additionalItemsB,
op: 'AND',
},

View File

@ -81,8 +81,8 @@ export const Query = memo(function Query({
}, [handleChangeQueryData, query]);
const handleChangeTagFilters = useCallback(
(value: IBuilderQuery['tagFilters']) => {
handleChangeQueryData('tagFilters', value);
(value: IBuilderQuery['filters']) => {
handleChangeQueryData('filters', value);
},
[handleChangeQueryData],
);

View File

@ -34,7 +34,6 @@ export function HavingFilter({
);
const { isMulti } = useTagValidation(
searchText,
currentFormValue.op,
currentFormValue.value,
);

View File

@ -1,15 +1,23 @@
import { Select, Spin, Tag, Tooltip } from 'antd';
import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete';
import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues';
import React, { useEffect, useMemo } from 'react';
import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import { selectStyle } from './config';
import { StyledCheckOutlined, TypographyText } from './style';
import { isInNotInOperator } from './utils';
import {
getOperatorValue,
getRemovePrefixFromKey,
getTagToken,
isExistsNotExistsOperator,
isInNInOperator,
} from './utils';
function QueryBuilderSearch({
query,
@ -26,18 +34,27 @@ function QueryBuilderSearch({
searchValue,
isMulti,
isFetching,
setSearchKey,
searchKey,
} = useAutoComplete(query);
const { keys } = useFetchKeysAndValues(searchValue, query, searchKey);
const onTagRender = ({
value,
closable,
onClose,
}: CustomTagProps): React.ReactElement => {
const isInNin = isInNotInOperator(value);
const { tagOperator } = getTagToken(value);
const isInNin = isInNInOperator(tagOperator);
const chipValue = isInNin
? value?.trim()?.replace(/,\s*$/, '')
: value?.trim();
const onCloseHandler = (): void => {
onClose();
handleSearch('');
setSearchKey('');
};
const tagEditHandler = (value: string): void => {
@ -46,14 +63,16 @@ function QueryBuilderSearch({
};
return (
<Tag closable={closable} onClose={onCloseHandler}>
<Tooltip title={value}>
<Tag closable={!searchValue && closable} onClose={onCloseHandler}>
<Tooltip title={chipValue}>
<TypographyText
ellipsis
$isInNin={isInNin}
disabled={!!searchValue}
$isEnabled={!!searchValue}
onClick={(): void => tagEditHandler(value)}
>
{value}
{chipValue}
</TypographyText>
</Tooltip>
</Tag>
@ -66,43 +85,57 @@ function QueryBuilderSearch({
const onInputKeyDownHandler = (event: React.KeyboardEvent<Element>): void => {
if (isMulti || event.key === 'Backspace') handleKeyDown(event);
if (isExistsNotExistsOperator(searchValue)) handleKeyDown(event);
};
const isMatricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
const queryTags = useMemo(() => {
if (!query.aggregateAttribute.key) return [];
if (!query.aggregateAttribute.key && isMatricsDataSource) return [];
return tags;
}, [query.aggregateAttribute.key, tags]);
}, [isMatricsDataSource, query.aggregateAttribute.key, tags]);
useEffect(() => {
const initialTagFilters: TagFilter = { items: [], op: 'AND' };
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
initialTagFilters.items = tags.map((tag) => {
const [tagKey, tagOperator, ...tagValue] = tag.split(' ');
const { tagKey, tagOperator, tagValue } = getTagToken(tag);
const filterAttribute = (keys || []).find(
(key) => key.key === getRemovePrefixFromKey(tagKey),
);
return {
id: uuid().slice(0, 8),
// TODO: key should be fixed by Chintan Sudani
key: tagKey,
op: tagOperator,
value: tagValue.map((i) => i.replace(',', '')),
key: filterAttribute ?? {
key: tagKey,
dataType: null,
type: null,
isColumn: null,
},
op: getOperatorValue(tagOperator),
value:
tagValue[tagValue.length - 1] === ''
? tagValue?.slice(0, -1)
: tagValue ?? '',
};
});
onChange(initialTagFilters);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tags]);
}, [keys, tags]);
return (
<Select
virtual
showSearch
tagRender={onTagRender}
filterOption={!isMulti}
filterOption={false}
autoClearSearchValue={false}
mode="multiple"
placeholder="Search Filter"
value={queryTags}
searchValue={searchValue}
disabled={!query.aggregateAttribute.key}
disabled={isMatricsDataSource && !query.aggregateAttribute.key}
style={selectStyle}
onSearch={handleSearch}
onChange={onChangeHandler}
@ -111,9 +144,9 @@ function QueryBuilderSearch({
onInputKeyDown={onInputKeyDownHandler}
notFoundContent={isFetching ? <Spin size="small" /> : null}
>
{options?.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.value}
{options.map((option) => (
<Select.Option key={option.label} value={option.label}>
{option.label}
{option.selected && <StyledCheckOutlined />}
</Select.Option>
))}

View File

@ -2,9 +2,14 @@ import { CheckOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import styled from 'styled-components';
export const TypographyText = styled(Typography.Text)<{ $isInNin: boolean }>`
export const TypographyText = styled(Typography.Text)<{
$isInNin: boolean;
$isEnabled: boolean;
}>`
width: ${({ $isInNin }): string => ($isInNin ? '10rem' : 'auto')};
cursor: pointer;
cursor: ${({ $isEnabled }): string =>
$isEnabled ? 'not-allowed' : 'pointer'};
pointer-events: ${({ $isEnabled }): string => ($isEnabled ? 'none' : 'auto')};
`;
export const StyledCheckOutlined = styled(CheckOutlined)`

View File

@ -1,9 +1,108 @@
import { OPERATORS } from 'constants/queryBuilder';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as Papa from 'papaparse';
export function isInNotInOperator(value: string): boolean {
return value?.includes(OPERATORS.IN || OPERATORS.NIN);
export const tagRegexp = /([a-zA-Z0-9_.:@$()\-/\\]+)\s*(!=|=|<=|<|>=|>|IN|NOT_IN|LIKE|NOT_LIKE|EXISTS|NOT_EXISTS|CONTAINS|NOT_CONTAINS)\s*([\s\S]*)/g;
export function isInNInOperator(value: string): boolean {
return value === OPERATORS.IN || value === OPERATORS.NIN;
}
interface ITagToken {
tagKey: string;
tagOperator: string;
tagValue: string[];
}
export function getTagToken(tag: string): ITagToken {
const matches = tag?.matchAll(tagRegexp);
const [match] = matches ? Array.from(matches) : [];
if (match) {
const [, matchTagKey, matchTagOperator, matchTagValue] = match;
return {
tagKey: matchTagKey,
tagOperator: matchTagOperator,
tagValue: isInNInOperator(matchTagOperator)
? Papa.parse(matchTagValue).data.flat()
: matchTagValue,
} as ITagToken;
}
return {
tagKey: tag,
tagOperator: '',
tagValue: [],
};
}
export function isExistsNotExistsOperator(value: string): boolean {
return value?.includes(OPERATORS.EXISTS || OPERATORS.NOT_EXISTS);
const { tagOperator } = getTagToken(value);
return (
tagOperator === OPERATORS.NOT_EXISTS || tagOperator === OPERATORS.EXISTS
);
}
export function getRemovePrefixFromKey(tag: string): string {
return tag?.replace(/^(tag_|resource_)/, '');
}
export function getOperatorValue(op: string): string {
switch (op) {
case 'IN':
return 'in';
case 'NOT_IN':
return 'nin';
case 'LIKE':
return 'like';
case 'NOT_LIKE':
return 'nlike';
case 'EXISTS':
return 'exists';
case 'NOT_EXISTS':
return 'nexists';
case 'CONTAINS':
return 'contains';
case 'NOT_CONTAINS':
return 'ncontains';
default:
return op;
}
}
export function getOperatorFromValue(op: string): string {
switch (op) {
case 'in':
return 'IN';
case 'nin':
return 'NOT_IN';
case 'like':
return 'LIKE';
case 'nlike':
return 'NOT_LIKE';
case 'exists':
return 'EXISTS';
case 'nexists':
return 'NOT_EXISTS';
case 'contains':
return 'CONTAINS';
case 'ncontains':
return 'NOT_CONTAINS';
default:
return op;
}
}
export function replaceStringWithMaxLength(
mainString: string,
array: string[],
replacementString: string,
): string {
const lastSearchValue = array.pop() ?? ''; // Remove the last search value from the array
if (lastSearchValue === '') return `${mainString}${replacementString},`; // if user select direclty from options
return mainString.replace(lastSearchValue, `${replacementString},`);
}
export function checkCommaInValue(str: string): string {
return str.includes(',') ? `"${str}"` : str;
}

View File

@ -12,5 +12,6 @@ export interface ContextValueI {
export type Option = {
value: string;
label: string;
selected?: boolean;
};

View File

@ -13,6 +13,7 @@ function CustomDateTimeModal({
}: CustomDateTimeModalProps): JSX.Element {
const [selectedDate, setDateTime] = useState<DateTimeRangeType>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onModalOkHandler = (date_time: any): void => {
onCreate(date_time);
setDateTime(date_time);

View File

@ -1,8 +1,14 @@
import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import {
getRemovePrefixFromKey,
getTagToken,
isExistsNotExistsOperator,
replaceStringWithMaxLength,
tagRegexp,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { Option } from 'container/QueryBuilder/type';
import * as Papa from 'papaparse';
import { useCallback, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { checkStringEndsWithSpace } from 'utils/checkStringEndsWithSpace';
import { useFetchKeysAndValues } from './useFetchKeysAndValues';
import { useOptions } from './useOptions';
@ -21,52 +27,56 @@ interface IAutoComplete {
searchValue: string;
isMulti: boolean;
isFetching: boolean;
setSearchKey: (value: string) => void;
searchKey: string;
}
export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>('');
const handleSearch = (value: string): void => setSearchValue(value);
const [searchKey, setSearchKey] = useState<string>('');
const { keys, results, isFetching } = useFetchKeysAndValues(
searchValue,
query,
searchKey,
);
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
const {
isValidTag,
isExist,
isValidOperator,
isMulti,
isFreeText,
} = useTagValidation(searchValue, operator, result);
const handleSearch = (value: string): void => {
const prefixFreeValue = getRemovePrefixFromKey(getTagToken(value).tagKey);
setSearchValue(value);
setSearchKey(prefixFreeValue);
};
const { isValidTag, isExist, isValidOperator, isMulti } = useTagValidation(
operator,
result,
);
const { handleAddTag, handleClearTag, tags, updateTag } = useTag(
key,
isValidTag,
isFreeText,
handleSearch,
query,
setSearchKey,
);
const handleSelect = useCallback(
(value: string): void => {
if (isMulti) {
setSearchValue((prev: string) => {
if (prev.includes(value)) {
return prev.replace(` ${value}`, '');
}
return checkStringEndsWithSpace(prev)
? `${prev} ${value}`
: `${prev} ${value},`;
const matches = prev?.matchAll(tagRegexp);
const [match] = matches ? Array.from(matches) : [];
const [, , , matchTagValue] = match;
const data = Papa.parse(matchTagValue).data.flat();
return replaceStringWithMaxLength(prev, data as string[], value);
});
}
if (!isMulti && isValidTag && !isExistsNotExistsOperator(value)) {
handleAddTag(value);
}
if (!isMulti && isExistsNotExistsOperator(value)) {
if (!isMulti && isValidTag && isExistsNotExistsOperator(value)) {
handleAddTag(value);
}
},
@ -83,7 +93,7 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
}
if (event.key === 'Enter' && searchValue && isValidTag) {
if (isMulti || isFreeText) {
if (isMulti) {
event.stopPropagation();
}
event.preventDefault();
@ -96,15 +106,7 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
handleClearTag(last);
}
},
[
handleAddTag,
handleClearTag,
isFreeText,
isMulti,
isValidTag,
searchValue,
tags,
],
[handleAddTag, handleClearTag, isMulti, isValidTag, searchValue, tags],
);
const options = useOptions(
@ -130,5 +132,7 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
searchValue,
isMulti,
isFetching,
setSearchKey,
searchKey,
};
};

View File

@ -1,16 +1,20 @@
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import {
AttributeKeyOptions,
getAttributesKeys,
getAttributesValues,
} from 'api/queryBuilder/getAttributesKeysValues';
import { useEffect, useRef, useState } from 'react';
getRemovePrefixFromKey,
getTagToken,
isInNInOperator,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useDebounce } from 'react-use';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { separateSearchValue } from 'utils/separateSearchValue';
import { DataSource } from 'types/common/queryBuilder';
type UseFetchKeysAndValuesReturnValues = {
keys: AttributeKeyOptions[];
type IuseFetchKeysAndValues = {
keys: BaseAutocompleteData[];
results: string[];
isFetching: boolean;
};
@ -25,25 +29,44 @@ type UseFetchKeysAndValuesReturnValues = {
export const useFetchKeysAndValues = (
searchValue: string,
query: IBuilderQuery,
): UseFetchKeysAndValuesReturnValues => {
const [keys, setKeys] = useState<AttributeKeyOptions[]>([]);
searchKey: string,
): IuseFetchKeysAndValues => {
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
const [results, setResults] = useState<string[]>([]);
const isQueryEnabled = useMemo(
() =>
query.dataSource === DataSource.METRICS
? !!query.aggregateOperator &&
!!query.dataSource &&
!!query.aggregateAttribute.dataType
: true,
[
query.aggregateAttribute.dataType,
query.aggregateOperator,
query.dataSource,
],
);
const { data, isFetching, status } = useQuery(
[
'GET_ATTRIBUTE_KEY',
searchValue,
QueryBuilderKeys.GET_ATTRIBUTE_KEY,
searchKey,
query.dataSource,
query.aggregateOperator,
query.aggregateAttribute.key,
],
async () =>
getAttributesKeys({
searchText: searchValue,
getAggregateKeys({
searchText: searchKey,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
tagType: query.aggregateAttribute.type ?? null,
}),
{ enabled: !!query.aggregateOperator && !!query.dataSource },
{
enabled: isQueryEnabled,
},
);
/**
@ -54,28 +77,36 @@ export const useFetchKeysAndValues = (
const handleFetchOption = async (
value: string,
query: IBuilderQuery,
keys: BaseAutocompleteData[],
): Promise<void> => {
if (value) {
// separate the search value into the attribute key and the operator
const [tKey, operator] = separateSearchValue(value);
setResults([]);
if (tKey && operator) {
const { payload } = await getAttributesValues({
searchText: searchValue,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
attributeKey: tKey,
});
if (payload) {
const values = Object.values(payload).find((el) => !!el);
if (values) {
setResults(values);
} else {
setResults([]);
}
}
}
if (!value) {
return;
}
const { tagKey, tagOperator, tagValue } = getTagToken(value);
const filterAttributeKey = keys.find(
(item) => item.key === getRemovePrefixFromKey(tagKey),
);
setResults([]);
if (!tagKey || !tagOperator) {
return;
}
const { payload } = await getAttributesValues({
aggregateOperator: query.aggregateOperator,
dataSource: query.dataSource,
aggregateAttribute: query.aggregateAttribute.key,
attributeKey: filterAttributeKey?.key ?? tagKey,
filterAttributeKeyDataType: filterAttributeKey?.dataType ?? null,
tagType: filterAttributeKey?.type ?? null,
searchText: isInNInOperator(tagOperator)
? tagValue[tagValue.length - 1]?.toString() ?? '' // last element of tagvalue will be always user search value
: tagValue?.toString() ?? '',
});
if (payload) {
const values = Object.values(payload).find((el) => !!el) || [];
setResults(values);
}
};
@ -83,20 +114,21 @@ export const useFetchKeysAndValues = (
const clearFetcher = useRef(handleFetchOption).current;
// debounces the fetch function to avoid excessive API calls
useDebounce(() => clearFetcher(searchValue, query), 500, [
useDebounce(() => clearFetcher(searchValue, query, keys), 750, [
clearFetcher,
searchValue,
query,
keys,
]);
// update the fetched keys when the fetch status changes
useEffect(() => {
if (status === 'success' && data?.payload) {
setKeys(data?.payload);
if (status === 'success' && data?.payload?.attributeKeys) {
setKeys(data?.payload.attributeKeys);
} else {
setKeys([]);
}
}, [data?.payload, status]);
}, [data?.payload?.attributeKeys, status]);
return {
keys,

View File

@ -1,20 +1,22 @@
import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
import { QUERY_BUILDER_OPERATORS_BY_TYPES } from 'constants/queryBuilder';
import { getRemovePrefixFromKey } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useMemo } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
type IOperators =
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.universal
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.string
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.boolean
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.number;
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.bool
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.int64
| typeof QUERY_BUILDER_OPERATORS_BY_TYPES.float64;
export const useOperators = (
key: string,
keys: AttributeKeyOptions[],
keys: BaseAutocompleteData[],
): IOperators =>
useMemo(() => {
const currentKey = keys?.find((el) => el.key === key);
return currentKey
const currentKey = keys?.find((el) => el.key === getRemovePrefixFromKey(key));
return currentKey?.dataType
? QUERY_BUILDER_OPERATORS_BY_TYPES[currentKey.dataType]
: QUERY_BUILDER_OPERATORS_BY_TYPES.universal;
}, [keys, key]);

View File

@ -1,12 +1,17 @@
import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
import {
checkCommaInValue,
getTagToken,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { Option } from 'container/QueryBuilder/type';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { useOperators } from './useOperators';
export const useOptions = (
key: string,
keys: AttributeKeyOptions[],
keys: BaseAutocompleteData[],
operator: string,
searchValue: string,
isMulti: boolean,
@ -18,37 +23,73 @@ export const useOptions = (
const [options, setOptions] = useState<Option[]>([]);
const operators = useOperators(key, keys);
const updateOptions = useCallback(() => {
const getLabel = useCallback(
(data: BaseAutocompleteData): Option['label'] =>
transformStringWithPrefix({
str: data?.key,
prefix: data?.type || '',
condition: !data?.isColumn,
}),
[],
);
const getOptionsFromKeys = useCallback(
(items: BaseAutocompleteData[]): Option[] =>
items?.map((item) => ({
label: `${getLabel(item)}`,
value: item.key,
})),
[getLabel],
);
const getKeyOpValue = useCallback(
(items: string[]): Option[] =>
items?.map((item) => ({
label: `${key} ${operator} ${item}`,
value: `${key} ${operator} ${item}`,
})),
[key, operator],
);
useEffect(() => {
if (!key) {
setOptions(
searchValue
? [{ value: searchValue }, ...keys.map((k) => ({ value: k.key }))]
: keys?.map((k) => ({ value: k.key })),
? [
{ label: `${searchValue} `, value: `${searchValue} ` },
...getOptionsFromKeys(keys),
]
: getOptionsFromKeys(keys),
);
} else if (key && !operator) {
setOptions(
operators?.map((o) => ({
value: `${key} ${o}`,
label: `${key} ${o.replace('_', ' ')}`,
operators?.map((operator) => ({
value: `${key} ${operator} `,
label: `${key} ${operator} `,
})),
);
} else if (key && operator) {
if (isMulti) {
setOptions(results.map((r) => ({ value: `${r}` })));
setOptions(
results.map((item) => ({
label: checkCommaInValue(String(item)),
value: String(item),
})),
);
} else if (isExist) {
setOptions([]);
} else if (isValidOperator) {
const hasAllResults = result.every((val) => results.includes(val));
const values = results.map((r) => ({
value: `${key} ${operator} ${r}`,
}));
const hasAllResults = results.every((value) => result.includes(value));
const values = getKeyOpValue(results);
const options = hasAllResults
? values
: [{ value: searchValue }, ...values];
? [{ label: searchValue, value: searchValue }]
: [{ label: searchValue, value: searchValue }, ...values];
setOptions(options);
}
}
}, [
getKeyOpValue,
getOptionsFromKeys,
isExist,
isMulti,
isValidOperator,
@ -61,15 +102,25 @@ export const useOptions = (
searchValue,
]);
useEffect(() => {
updateOptions();
}, [updateOptions]);
return useMemo(
() =>
options?.map((option) => {
(
options.filter(
(option, index, self) =>
index ===
self.findIndex(
(o) => o.label === option.label && o.value === option.value, // to remove duplicate & empty options from list
) && option.value !== '',
) || []
).map((option) => {
const { tagValue } = getTagToken(searchValue);
if (isMulti) {
return { ...option, selected: searchValue.includes(option.value) };
return {
...option,
selected: tagValue
.filter((i) => i.trim().replace(/^\s+/, '') === option.value)
.includes(option.value),
};
}
return option;
}),

View File

@ -44,7 +44,7 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
having: [],
orderBy: [],
limit: null,
tagFilters: { items: [], op: 'AND' },
filters: { items: [], op: 'AND' },
...(shouldResetAggregateAttribute
? { aggregateAttribute: initialAggregateAttribute }
: {}),

View File

@ -1,13 +1,15 @@
import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
import {
getRemovePrefixFromKey,
getTagToken,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useMemo } from 'react';
import { getCountOfSpace } from 'utils/getCountOfSpace';
import { separateSearchValue } from 'utils/separateSearchValue';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
type ICurrentKeyAndOperator = [string, string, string[]];
export const useSetCurrentKeyAndOperator = (
value: string,
keys: AttributeKeyOptions[],
keys: BaseAutocompleteData[],
): ICurrentKeyAndOperator => {
const [key, operator, result] = useMemo(() => {
let key = '';
@ -15,13 +17,14 @@ export const useSetCurrentKeyAndOperator = (
let result: string[] = [];
if (value) {
const [tKey, tOperator, tResult] = separateSearchValue(value);
const isSuggestKey = keys?.some((el) => el.key === tKey);
if (getCountOfSpace(value) >= 1 || isSuggestKey) {
key = tKey || '';
operator = tOperator || '';
result = tResult.filter((el) => el);
const { tagKey, tagOperator, tagValue } = getTagToken(value);
const isSuggestKey = keys?.some(
(el) => el?.key === getRemovePrefixFromKey(tagKey),
);
if (isSuggestKey || keys.length === 0) {
key = tagKey || '';
operator = tagOperator || '';
result = tagValue || [];
}
}

View File

@ -1,7 +1,10 @@
import {
getOperatorFromValue,
isExistsNotExistsOperator,
isInNotInOperator,
isInNInOperator,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as Papa from 'papaparse';
import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@ -16,16 +19,16 @@ type IUseTag = {
* A custom React hook for handling tags.
* @param {string} key - A string value to identify tags.
* @param {boolean} isValidTag - A boolean value to indicate whether the tag is valid.
* @param {boolean} isFreeText - A boolean value to indicate whether free text is allowed.
* @param {function} handleSearch - A callback function to handle search.
* @returns {IUseTag} The return object containing handlers and tags.
*/
export const useTag = (
key: string,
isValidTag: boolean,
isFreeText: boolean,
handleSearch: (value: string) => void,
query: IBuilderQuery,
setSearchKey: (value: string) => void,
): IUseTag => {
const [tags, setTags] = useState<string[]>([]);
@ -40,16 +43,13 @@ export const useTag = (
*/
const handleAddTag = useCallback(
(value: string): void => {
if (
(value && key && isValidTag) ||
isFreeText ||
isExistsNotExistsOperator(value)
) {
if ((value && key && isValidTag) || isExistsNotExistsOperator(value)) {
setTags((prevTags) => [...prevTags, value]);
handleSearch('');
setSearchKey('');
}
},
[key, isValidTag, isFreeText, handleSearch],
[key, isValidTag, handleSearch, setSearchKey],
);
/**
@ -61,13 +61,14 @@ export const useTag = (
}, []);
useEffect(() => {
setTags(
(query?.tagFilters?.items || []).map((obj) =>
isInNotInOperator(obj.op)
? `${obj.key} ${obj.op} ${obj.value.join(',')}`
: `${obj.key} ${obj.op} ${obj.value.join(' ')}`,
),
);
const initialTags = (query?.filters?.items || []).map((ele) => {
if (isInNInOperator(getOperatorFromValue(ele.op))) {
const csvString = Papa.unparse([ele.value]);
return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${csvString}`;
}
return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${ele.value}`;
});
setTags(initialTags);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

View File

@ -1,6 +1,5 @@
import { QUERY_BUILDER_SEARCH_VALUES } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { checkStringEndsWithSpace } from 'utils/checkStringEndsWithSpace';
import { useIsValidTag } from './useIsValidTag';
import { useOperatorType } from './useOperatorType';
@ -10,26 +9,25 @@ type ITagValidation = {
isExist: boolean;
isValidOperator: boolean;
isMulti: boolean;
isFreeText: boolean;
};
export const useTagValidation = (
value: string,
operator: string,
result: string[],
): ITagValidation => {
const operatorType = useOperatorType(operator);
const isValidTag = useIsValidTag(operatorType, result.length);
const resultLength =
operatorType === 'SINGLE_VALUE' ? [result]?.length : result?.length;
const isValidTag = useIsValidTag(operatorType, resultLength);
const { isExist, isValidOperator, isMulti, isFreeText } = useMemo(() => {
const { isExist, isValidOperator, isMulti } = useMemo(() => {
const isExist = operatorType === QUERY_BUILDER_SEARCH_VALUES.NON;
const isValidOperator =
operatorType !== QUERY_BUILDER_SEARCH_VALUES.NOT_VALID;
const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY;
const isFreeText = operator === '' && !checkStringEndsWithSpace(value);
return { isExist, isValidOperator, isMulti, isFreeText };
}, [operator, operatorType, value]);
return { isExist, isValidOperator, isMulti };
}, [operatorType]);
return { isValidTag, isExist, isValidOperator, isMulti, isFreeText };
return { isValidTag, isExist, isValidOperator, isMulti };
};

View File

@ -1,8 +1,11 @@
import { DataSource } from 'types/common/queryBuilder';
import { BaseAutocompleteData } from './queryAutocompleteResponse';
export interface IGetAttributeKeysPayload {
aggregateOperator: string;
dataSource: DataSource;
searchText: string;
aggregateAttribute: string;
tagType?: BaseAutocompleteData['type'];
}

View File

@ -0,0 +1,19 @@
import { DataSource } from 'types/common/queryBuilder';
import { BaseAutocompleteData } from './queryAutocompleteResponse';
export interface IGetAttributeValuesPayload {
dataSource: DataSource;
aggregateOperator: string;
aggregateAttribute: string;
searchText: string;
attributeKey: string;
filterAttributeKeyDataType: BaseAutocompleteData['dataType'];
tagType: BaseAutocompleteData['type'];
}
export interface IAttributeValuesResponse {
boolAttributeValues: null | string[];
numberAttributeValues: null | string[];
stringAttributeValues: null | string[];
}

View File

@ -15,12 +15,11 @@ export interface TagFilterItem {
id: string;
key?: BaseAutocompleteData;
op: string;
value: string[];
value: string[] | string;
}
export interface TagFilter {
items: TagFilterItem[];
// TODO: type it in the future
op: string;
}
@ -40,7 +39,7 @@ export type IBuilderQuery = {
dataSource: DataSource;
aggregateOperator: string;
aggregateAttribute: BaseAutocompleteData;
tagFilters: TagFilter;
filters: TagFilter;
groupBy: BaseAutocompleteData[];
expression: string;
disabled: boolean;

View File

@ -1,4 +0,0 @@
export const checkStringEndsWithSpace = (str: string): boolean => {
const endSpace = / $/;
return endSpace.test(str);
};

View File

@ -1 +0,0 @@
export const getCountOfSpace = (s: string): number => s.split(' ').length - 1;

View File

@ -1,9 +0,0 @@
export const getSearchParams = (newParams: {
[key: string]: string;
}): URLSearchParams => {
const params = new URLSearchParams();
Object.entries(newParams).forEach(([key, value]) => {
params.set(key, value);
});
return params;
};

View File

@ -1,12 +0,0 @@
import { OPERATORS } from 'constants/queryBuilder';
export const separateSearchValue = (
value: string,
): [string, string, string[]] => {
const separatedString = value.split(' ');
const [key, operator, ...result] = separatedString;
if (operator === OPERATORS.IN || operator === OPERATORS.NIN) {
return [key, operator, result];
}
return [key, operator, Array(result.join(' '))];
};