mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-15 02:46:02 +08:00
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:
parent
10e47b5bff
commit
76331001b7
@ -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",
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
42
frontend/src/api/queryBuilder/getAttributesValues.ts
Normal file
42
frontend/src/api/queryBuilder/getAttributesValues.ts
Normal 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);
|
||||
}
|
||||
};
|
@ -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,
|
||||
|
@ -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',
|
||||
},
|
||||
|
@ -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],
|
||||
);
|
||||
|
@ -34,7 +34,6 @@ export function HavingFilter({
|
||||
);
|
||||
|
||||
const { isMulti } = useTagValidation(
|
||||
searchText,
|
||||
currentFormValue.op,
|
||||
currentFormValue.value,
|
||||
);
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -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)`
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -12,5 +12,6 @@ export interface ContextValueI {
|
||||
|
||||
export type Option = {
|
||||
value: string;
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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]);
|
||||
|
@ -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;
|
||||
}),
|
||||
|
@ -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 }
|
||||
: {}),
|
||||
|
@ -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 || [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}, []);
|
||||
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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'];
|
||||
}
|
||||
|
19
frontend/src/types/api/queryBuilder/getAttributesValues.ts
Normal file
19
frontend/src/types/api/queryBuilder/getAttributesValues.ts
Normal 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[];
|
||||
}
|
@ -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;
|
||||
|
@ -1,4 +0,0 @@
|
||||
export const checkStringEndsWithSpace = (str: string): boolean => {
|
||||
const endSpace = / $/;
|
||||
return endSpace.test(str);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export const getCountOfSpace = (s: string): number => s.split(' ').length - 1;
|
@ -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;
|
||||
};
|
@ -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(' '))];
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user