mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-10 05:49:03 +08:00
feat: Search bar for Query Builder (#2517)
This commit is contained in:
parent
2c206e8bf4
commit
23081996c4
63
frontend/src/api/queryBuilder/getAttributesKeysValues.ts
Normal file
63
frontend/src/api/queryBuilder/getAttributesKeysValues.ts
Normal file
@ -0,0 +1,63 @@
|
||||
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);
|
||||
}
|
||||
};
|
@ -61,3 +61,78 @@ export const operatorsByTypes: Record<LocalDataType, string[]> = {
|
||||
number: Object.values(NumberOperators),
|
||||
bool: Object.values(BoolOperators),
|
||||
};
|
||||
|
||||
export type IQueryBuilderState = 'search';
|
||||
|
||||
export const QUERY_BUILDER_SEARCH_VALUES = {
|
||||
MULTIPLY: 'MULTIPLY_VALUE',
|
||||
SINGLE: 'SINGLE_VALUE',
|
||||
NON: 'NON_VALUE',
|
||||
NOT_VALID: 'NOT_VALID',
|
||||
};
|
||||
|
||||
export const OPERATORS = {
|
||||
IN: 'IN',
|
||||
NIN: 'NOT_IN',
|
||||
LIKE: 'LIKE',
|
||||
NLIKE: 'NOT_LIKE',
|
||||
EQUALS: '=',
|
||||
NOT_EQUALS: '!=',
|
||||
EXISTS: 'EXISTS',
|
||||
NOT_EXISTS: 'NOT_EXISTS',
|
||||
CONTAINS: 'CONTAINS',
|
||||
NOT_CONTAINS: 'NOT_CONTAINS',
|
||||
GTE: '>=',
|
||||
GT: '>',
|
||||
LTE: '<=',
|
||||
LT: '<',
|
||||
};
|
||||
|
||||
export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
||||
string: [
|
||||
OPERATORS.EQUALS,
|
||||
OPERATORS.NOT_EQUALS,
|
||||
OPERATORS.IN,
|
||||
OPERATORS.NIN,
|
||||
OPERATORS.LIKE,
|
||||
OPERATORS.NLIKE,
|
||||
OPERATORS.CONTAINS,
|
||||
OPERATORS.NOT_CONTAINS,
|
||||
OPERATORS.EXISTS,
|
||||
OPERATORS.NOT_EXISTS,
|
||||
],
|
||||
number: [
|
||||
OPERATORS.EQUALS,
|
||||
OPERATORS.NOT_EQUALS,
|
||||
OPERATORS.IN,
|
||||
OPERATORS.NIN,
|
||||
OPERATORS.EXISTS,
|
||||
OPERATORS.NOT_EXISTS,
|
||||
OPERATORS.GTE,
|
||||
OPERATORS.GT,
|
||||
OPERATORS.LTE,
|
||||
OPERATORS.LT,
|
||||
],
|
||||
boolean: [
|
||||
OPERATORS.EQUALS,
|
||||
OPERATORS.NOT_EQUALS,
|
||||
OPERATORS.EXISTS,
|
||||
OPERATORS.NOT_EXISTS,
|
||||
],
|
||||
universal: [
|
||||
OPERATORS.EQUALS,
|
||||
OPERATORS.NOT_EQUALS,
|
||||
OPERATORS.IN,
|
||||
OPERATORS.NIN,
|
||||
OPERATORS.EXISTS,
|
||||
OPERATORS.NOT_EXISTS,
|
||||
OPERATORS.LIKE,
|
||||
OPERATORS.NLIKE,
|
||||
OPERATORS.GTE,
|
||||
OPERATORS.GT,
|
||||
OPERATORS.LTE,
|
||||
OPERATORS.LT,
|
||||
OPERATORS.CONTAINS,
|
||||
OPERATORS.NOT_CONTAINS,
|
||||
],
|
||||
};
|
||||
|
@ -11,12 +11,14 @@ export const Container = styled.div`
|
||||
export const RightContainerWrapper = styled(Col)`
|
||||
&&& {
|
||||
min-width: 200px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LeftContainerWrapper = styled(Col)`
|
||||
&&& {
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
max-width: 70%;
|
||||
}
|
||||
`;
|
||||
|
@ -7,7 +7,7 @@ export const StyledButton = styled(Button)<{ $isAvailableToDisable: boolean }>`
|
||||
padding: ${(props): string =>
|
||||
props.$isAvailableToDisable ? '0.43rem' : '0.43rem 0.68rem'};
|
||||
border-radius: 0.375rem;
|
||||
margin-right: 0.1rem;
|
||||
margin-right: 0.5rem;
|
||||
pointer-events: ${(props): string =>
|
||||
props.$isAvailableToDisable ? 'default' : 'none'};
|
||||
`;
|
||||
|
@ -2,10 +2,10 @@ import { Col, Input, Row } from 'antd';
|
||||
// ** Constants
|
||||
import {
|
||||
initialAggregateAttribute,
|
||||
initialQueryBuilderFormValues,
|
||||
mapOfFilters,
|
||||
mapOfOperators,
|
||||
} from 'constants/queryBuilder';
|
||||
import { initialQueryBuilderFormValues } from 'constants/queryBuilder';
|
||||
// ** Components
|
||||
import {
|
||||
AdditionalFiltersToggler,
|
||||
@ -19,7 +19,7 @@ import {
|
||||
OperatorsSelect,
|
||||
ReduceToFilter,
|
||||
} from 'container/QueryBuilder/filters';
|
||||
// Context
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
||||
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
|
||||
// ** Hooks
|
||||
@ -186,12 +186,17 @@ export const Query = memo(function Query({
|
||||
removeEntityByIndex('queryData', index);
|
||||
}, [removeEntityByIndex, index]);
|
||||
|
||||
const isMatricsDataSource = useMemo(
|
||||
() => query.dataSource === DataSource.METRICS,
|
||||
[query.dataSource],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRow gutter={[0, 15]}>
|
||||
<StyledDeleteEntity onClick={handleDeleteQuery} />
|
||||
<Col span={24}>
|
||||
<Row wrap={false} align="middle">
|
||||
<Col span={24}>
|
||||
<Row align="middle" justify="space-between">
|
||||
<Col>
|
||||
<ListMarker
|
||||
isDisabled={query.disabled}
|
||||
toggleDisabled={handleToggleDisableQuery}
|
||||
@ -203,11 +208,15 @@ export const Query = memo(function Query({
|
||||
<DataSourceDropdown
|
||||
onChange={handleChangeDataSource}
|
||||
value={query.dataSource}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
) : (
|
||||
<FilterLabel label={transformToUpperCase(query.dataSource)} />
|
||||
)}
|
||||
{/* TODO: here will be search */}
|
||||
{isMatricsDataSource && <FilterLabel label="WHERE" />}
|
||||
</Col>
|
||||
<Col span={isMatricsDataSource ? 17 : 20}>
|
||||
<QueryBuilderSearch query={query} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
@ -9,6 +9,7 @@ import { useQuery } from 'react-query';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
// ** Types
|
||||
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
|
||||
|
||||
@ -27,16 +28,15 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
],
|
||||
async () =>
|
||||
getAggregateAttribute({
|
||||
searchText,
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
dataSource: query.dataSource,
|
||||
searchText,
|
||||
}),
|
||||
{ enabled: !!query.aggregateOperator && !!query.dataSource },
|
||||
);
|
||||
|
||||
const handleSearchAttribute = (searchText: string): void => {
|
||||
const handleSearchAttribute = (searchText: string): void =>
|
||||
setSearchText(searchText);
|
||||
};
|
||||
|
||||
const optionsData: SelectOption<string, string>[] =
|
||||
data?.payload?.attributeKeys?.map((item) => ({
|
||||
@ -70,7 +70,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
<AutoComplete
|
||||
showSearch
|
||||
placeholder={`${transformToUpperCase(query.dataSource)} name`}
|
||||
style={{ width: '100%' }}
|
||||
style={selectStyle}
|
||||
showArrow={false}
|
||||
filterOption={false}
|
||||
onSearch={handleSearchAttribute}
|
||||
|
@ -0,0 +1 @@
|
||||
export const selectStyle = { width: '100%' };
|
@ -0,0 +1,96 @@
|
||||
import { Select, Spin, Tag, Tooltip } from 'antd';
|
||||
import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete';
|
||||
import React from 'react';
|
||||
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { selectStyle } from './config';
|
||||
import { StyledCheckOutlined, TypographyText } from './style';
|
||||
import { isInNotInOperator } from './utils';
|
||||
|
||||
function QueryBuilderSearch({ query }: QueryBuilderSearchProps): JSX.Element {
|
||||
const {
|
||||
handleClearTag,
|
||||
handleKeyDown,
|
||||
handleSearch,
|
||||
handleSelect,
|
||||
tags,
|
||||
options,
|
||||
searchValue,
|
||||
isMulti,
|
||||
isFetching,
|
||||
} = useAutoComplete(query);
|
||||
|
||||
const onTagRender = ({
|
||||
value,
|
||||
closable,
|
||||
onClose,
|
||||
}: CustomTagProps): React.ReactElement => {
|
||||
const isInNin = isInNotInOperator(value);
|
||||
|
||||
const onCloseHandler = (): void => {
|
||||
onClose();
|
||||
handleSearch('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag closable={closable} onClose={onCloseHandler}>
|
||||
<Tooltip title={value}>
|
||||
<TypographyText ellipsis isInNin={isInNin}>
|
||||
{value}
|
||||
</TypographyText>
|
||||
</Tooltip>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const onChangeHandler = (value: string[]): void => {
|
||||
if (!isMulti) handleSearch(value[value.length - 1]);
|
||||
};
|
||||
|
||||
const onInputKeyDownHandler = (event: React.KeyboardEvent<Element>): void => {
|
||||
if (isMulti || event.key === 'Backspace') handleKeyDown(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
virtual
|
||||
showSearch
|
||||
tagRender={onTagRender}
|
||||
filterOption={!isMulti}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
placeholder="Search Filter"
|
||||
value={tags}
|
||||
searchValue={searchValue}
|
||||
disabled={!query.aggregateAttribute.key}
|
||||
style={selectStyle}
|
||||
onSearch={handleSearch}
|
||||
onChange={onChangeHandler}
|
||||
onSelect={handleSelect}
|
||||
onDeselect={handleClearTag}
|
||||
onInputKeyDown={onInputKeyDownHandler}
|
||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||
>
|
||||
{options?.map((option) => (
|
||||
<Select.Option key={option.value} value={option.value}>
|
||||
{option.value}
|
||||
{option.selected && <StyledCheckOutlined />}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
interface QueryBuilderSearchProps {
|
||||
query: IBuilderQueryForm;
|
||||
}
|
||||
|
||||
export interface CustomTagProps {
|
||||
label: React.ReactNode;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
onClose: (event?: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
closable: boolean;
|
||||
}
|
||||
|
||||
export default QueryBuilderSearch;
|
@ -0,0 +1,11 @@
|
||||
import { CheckOutlined } from '@ant-design/icons';
|
||||
import { Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TypographyText = styled(Typography.Text)<{ isInNin: boolean }>`
|
||||
width: ${({ isInNin }): string => (isInNin ? '10rem' : 'auto')};
|
||||
`;
|
||||
|
||||
export const StyledCheckOutlined = styled(CheckOutlined)`
|
||||
float: right;
|
||||
`;
|
@ -0,0 +1,9 @@
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
|
||||
export function isInNotInOperator(value: string): boolean {
|
||||
return value?.includes(OPERATORS.IN || OPERATORS.NIN);
|
||||
}
|
||||
|
||||
export function isExistsNotExistsOperator(value: string): boolean {
|
||||
return value?.includes(OPERATORS.EXISTS || OPERATORS.NOT_EXISTS);
|
||||
}
|
16
frontend/src/container/QueryBuilder/type.ts
Normal file
16
frontend/src/container/QueryBuilder/type.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { IQueryBuilderState } from 'constants/queryBuilder';
|
||||
|
||||
export interface InitialStateI {
|
||||
search: string;
|
||||
}
|
||||
|
||||
export interface ContextValueI {
|
||||
values: InitialStateI;
|
||||
onChangeHandler: (type: IQueryBuilderState) => (value: string) => void;
|
||||
onSubmitHandler: VoidFunction;
|
||||
}
|
||||
|
||||
export type Option = {
|
||||
value: string;
|
||||
selected?: boolean;
|
||||
};
|
131
frontend/src/hooks/queryBuilder/useAutoComplete.ts
Normal file
131
frontend/src/hooks/queryBuilder/useAutoComplete.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { Option } from 'container/QueryBuilder/type';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { checkStringEndsWithSpace } from 'utils/checkStringEndsWithSpace';
|
||||
|
||||
import { useFetchKeysAndValues } from './useFetchKeysAndValues';
|
||||
import { useOptions } from './useOptions';
|
||||
import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator';
|
||||
import { useTag } from './useTag';
|
||||
import { useTagValidation } from './useTagValidation';
|
||||
|
||||
interface IAutoComplete {
|
||||
handleSearch: (value: string) => void;
|
||||
handleClearTag: (value: string) => void;
|
||||
handleSelect: (value: string) => void;
|
||||
handleKeyDown: (event: React.KeyboardEvent) => void;
|
||||
options: Option[];
|
||||
tags: string[];
|
||||
searchValue: string;
|
||||
isMulti: boolean;
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
export const useAutoComplete = (query: IBuilderQueryForm): IAutoComplete => {
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
|
||||
const handleSearch = (value: string): void => setSearchValue(value);
|
||||
|
||||
const { keys, results, isFetching } = useFetchKeysAndValues(
|
||||
searchValue,
|
||||
query,
|
||||
);
|
||||
|
||||
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
|
||||
|
||||
const {
|
||||
isValidTag,
|
||||
isExist,
|
||||
isValidOperator,
|
||||
isMulti,
|
||||
isFreeText,
|
||||
} = useTagValidation(searchValue, operator, result);
|
||||
|
||||
const { handleAddTag, handleClearTag, tags } = useTag(
|
||||
key,
|
||||
isValidTag,
|
||||
isFreeText,
|
||||
handleSearch,
|
||||
);
|
||||
|
||||
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}`;
|
||||
});
|
||||
}
|
||||
if (!isMulti && isValidTag && !isExistsNotExistsOperator(value)) {
|
||||
handleAddTag(value);
|
||||
}
|
||||
if (!isMulti && isExistsNotExistsOperator(value)) {
|
||||
handleAddTag(value);
|
||||
}
|
||||
},
|
||||
[handleAddTag, isMulti, isValidTag],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent): void => {
|
||||
if (
|
||||
event.key === ' ' &&
|
||||
(searchValue.endsWith(' ') || searchValue.length === 0)
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && searchValue && isValidTag) {
|
||||
if (isMulti || isFreeText) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
event.preventDefault();
|
||||
handleAddTag(searchValue);
|
||||
}
|
||||
|
||||
if (event.key === 'Backspace' && !searchValue) {
|
||||
event.stopPropagation();
|
||||
const last = tags[tags.length - 1];
|
||||
handleClearTag(last);
|
||||
}
|
||||
},
|
||||
[
|
||||
handleAddTag,
|
||||
handleClearTag,
|
||||
isFreeText,
|
||||
isMulti,
|
||||
isValidTag,
|
||||
searchValue,
|
||||
tags,
|
||||
],
|
||||
);
|
||||
|
||||
const options = useOptions(
|
||||
key,
|
||||
keys,
|
||||
operator,
|
||||
searchValue,
|
||||
isMulti,
|
||||
isValidOperator,
|
||||
isExist,
|
||||
results,
|
||||
result,
|
||||
);
|
||||
|
||||
return {
|
||||
handleSearch,
|
||||
handleClearTag,
|
||||
handleSelect,
|
||||
handleKeyDown,
|
||||
options,
|
||||
tags,
|
||||
searchValue,
|
||||
isMulti,
|
||||
isFetching,
|
||||
};
|
||||
};
|
106
frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts
Normal file
106
frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import {
|
||||
AttributeKeyOptions,
|
||||
getAttributesKeys,
|
||||
getAttributesValues,
|
||||
} from 'api/queryBuilder/getAttributesKeysValues';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { separateSearchValue } from 'utils/separateSearchValue';
|
||||
|
||||
type UseFetchKeysAndValuesReturnValues = {
|
||||
keys: AttributeKeyOptions[];
|
||||
results: string[];
|
||||
isFetching: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to fetch attribute keys and values from an API
|
||||
* @param searchValue - the search query value
|
||||
* @param query - an object containing data for the query
|
||||
* @returns an object containing the fetched attribute keys, results, and the status of the fetch
|
||||
*/
|
||||
|
||||
export const useFetchKeysAndValues = (
|
||||
searchValue: string,
|
||||
query: IBuilderQueryForm,
|
||||
): UseFetchKeysAndValuesReturnValues => {
|
||||
const [keys, setKeys] = useState<AttributeKeyOptions[]>([]);
|
||||
const [results, setResults] = useState<string[]>([]);
|
||||
const { data, isFetching, status } = useQuery(
|
||||
[
|
||||
'GET_ATTRIBUTE_KEY',
|
||||
searchValue,
|
||||
query.dataSource,
|
||||
query.aggregateOperator,
|
||||
query.aggregateAttribute.key,
|
||||
],
|
||||
async () =>
|
||||
getAttributesKeys({
|
||||
searchText: searchValue,
|
||||
dataSource: query.dataSource,
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
aggregateAttribute: query.aggregateAttribute.key,
|
||||
}),
|
||||
{ enabled: !!query.aggregateOperator && !!query.dataSource },
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetches the options to be displayed based on the selected value
|
||||
* @param value - the selected value
|
||||
* @param query - an object containing data for the query
|
||||
*/
|
||||
const handleFetchOption = async (
|
||||
value: string,
|
||||
query: IBuilderQueryForm,
|
||||
): 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([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// creates a ref to the fetch function so that it doesn't change on every render
|
||||
const clearFetcher = useRef(handleFetchOption).current;
|
||||
|
||||
// debounces the fetch function to avoid excessive API calls
|
||||
useDebounce(() => clearFetcher(searchValue, query), 500, [
|
||||
clearFetcher,
|
||||
searchValue,
|
||||
query,
|
||||
]);
|
||||
|
||||
// update the fetched keys when the fetch status changes
|
||||
useEffect(() => {
|
||||
if (status === 'success' && data?.payload) {
|
||||
setKeys(data?.payload);
|
||||
} else {
|
||||
setKeys([]);
|
||||
}
|
||||
}, [data?.payload, status]);
|
||||
|
||||
return {
|
||||
keys,
|
||||
results,
|
||||
isFetching,
|
||||
};
|
||||
};
|
22
frontend/src/hooks/queryBuilder/useIsValidTag.ts
Normal file
22
frontend/src/hooks/queryBuilder/useIsValidTag.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { OperatorType } from './useOperatorType';
|
||||
|
||||
const validationMapper: Record<
|
||||
OperatorType,
|
||||
(resultLength: number) => boolean
|
||||
> = {
|
||||
SINGLE_VALUE: (resultLength: number) => resultLength === 1,
|
||||
MULTIPLY_VALUE: (resultLength: number) => resultLength >= 1,
|
||||
NON_VALUE: (resultLength: number) => resultLength === 0,
|
||||
NOT_VALID: () => false,
|
||||
};
|
||||
|
||||
export const useIsValidTag = (
|
||||
operatorType: OperatorType,
|
||||
resultLength: number,
|
||||
): boolean =>
|
||||
useMemo(() => validationMapper[operatorType]?.(resultLength), [
|
||||
operatorType,
|
||||
resultLength,
|
||||
]);
|
27
frontend/src/hooks/queryBuilder/useOperatorType.ts
Normal file
27
frontend/src/hooks/queryBuilder/useOperatorType.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
|
||||
export type OperatorType =
|
||||
| 'SINGLE_VALUE'
|
||||
| 'MULTIPLY_VALUE'
|
||||
| 'NON_VALUE'
|
||||
| 'NOT_VALID';
|
||||
|
||||
const operatorTypeMapper: Record<string, OperatorType> = {
|
||||
[OPERATORS.IN]: 'MULTIPLY_VALUE',
|
||||
[OPERATORS.NIN]: 'MULTIPLY_VALUE',
|
||||
[OPERATORS.EXISTS]: 'NON_VALUE',
|
||||
[OPERATORS.NOT_EXISTS]: 'NON_VALUE',
|
||||
[OPERATORS.LTE]: 'SINGLE_VALUE',
|
||||
[OPERATORS.LT]: 'SINGLE_VALUE',
|
||||
[OPERATORS.GTE]: 'SINGLE_VALUE',
|
||||
[OPERATORS.GT]: 'SINGLE_VALUE',
|
||||
[OPERATORS.LIKE]: 'SINGLE_VALUE',
|
||||
[OPERATORS.NLIKE]: 'SINGLE_VALUE',
|
||||
[OPERATORS.CONTAINS]: 'SINGLE_VALUE',
|
||||
[OPERATORS.NOT_CONTAINS]: 'SINGLE_VALUE',
|
||||
[OPERATORS.EQUALS]: 'SINGLE_VALUE',
|
||||
[OPERATORS.NOT_EQUALS]: 'SINGLE_VALUE',
|
||||
};
|
||||
|
||||
export const useOperatorType = (operator: string): OperatorType =>
|
||||
operatorTypeMapper[operator] || 'NOT_VALID';
|
20
frontend/src/hooks/queryBuilder/useOperators.ts
Normal file
20
frontend/src/hooks/queryBuilder/useOperators.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
|
||||
import { QUERY_BUILDER_OPERATORS_BY_TYPES } from 'constants/queryBuilder';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
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;
|
||||
|
||||
export const useOperators = (
|
||||
key: string,
|
||||
keys: AttributeKeyOptions[],
|
||||
): IOperators =>
|
||||
useMemo(() => {
|
||||
const currentKey = keys?.find((el) => el.key === key);
|
||||
return currentKey
|
||||
? QUERY_BUILDER_OPERATORS_BY_TYPES[currentKey.dataType]
|
||||
: QUERY_BUILDER_OPERATORS_BY_TYPES.universal;
|
||||
}, [keys, key]);
|
78
frontend/src/hooks/queryBuilder/useOptions.ts
Normal file
78
frontend/src/hooks/queryBuilder/useOptions.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
|
||||
import { Option } from 'container/QueryBuilder/type';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useOperators } from './useOperators';
|
||||
|
||||
export const useOptions = (
|
||||
key: string,
|
||||
keys: AttributeKeyOptions[],
|
||||
operator: string,
|
||||
searchValue: string,
|
||||
isMulti: boolean,
|
||||
isValidOperator: boolean,
|
||||
isExist: boolean,
|
||||
results: string[],
|
||||
result: string[],
|
||||
): Option[] => {
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const operators = useOperators(key, keys);
|
||||
|
||||
const updateOptions = useCallback(() => {
|
||||
if (!key) {
|
||||
setOptions(
|
||||
searchValue
|
||||
? [{ value: searchValue }, ...keys.map((k) => ({ value: k.key }))]
|
||||
: keys?.map((k) => ({ value: k.key })),
|
||||
);
|
||||
} else if (key && !operator) {
|
||||
setOptions(
|
||||
operators.map((o) => ({
|
||||
value: `${key} ${o}`,
|
||||
label: `${key} ${o.replace('_', ' ')}`,
|
||||
})),
|
||||
);
|
||||
} else if (key && operator) {
|
||||
if (isMulti) {
|
||||
setOptions(results.map((r) => ({ value: `${r}` })));
|
||||
} 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 options = hasAllResults
|
||||
? values
|
||||
: [{ value: searchValue }, ...values];
|
||||
setOptions(options);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isExist,
|
||||
isMulti,
|
||||
isValidOperator,
|
||||
key,
|
||||
keys,
|
||||
operator,
|
||||
operators,
|
||||
result,
|
||||
results,
|
||||
searchValue,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
updateOptions();
|
||||
}, [updateOptions]);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
options?.map((option) => {
|
||||
if (isMulti) {
|
||||
return { ...option, selected: searchValue.includes(option.value) };
|
||||
}
|
||||
return option;
|
||||
}),
|
||||
[isMulti, options, searchValue],
|
||||
);
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
import { AttributeKeyOptions } from 'api/queryBuilder/getAttributesKeysValues';
|
||||
import { useMemo } from 'react';
|
||||
import { getCountOfSpace } from 'utils/getCountOfSpace';
|
||||
import { separateSearchValue } from 'utils/separateSearchValue';
|
||||
|
||||
type ICurrentKeyAndOperator = [string, string, string[]];
|
||||
|
||||
export const useSetCurrentKeyAndOperator = (
|
||||
value: string,
|
||||
keys: AttributeKeyOptions[],
|
||||
): ICurrentKeyAndOperator => {
|
||||
const [key, operator, result] = useMemo(() => {
|
||||
let key = '';
|
||||
let operator = '';
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return [key, operator, result];
|
||||
}, [value, keys]);
|
||||
|
||||
return [key, operator, result];
|
||||
};
|
53
frontend/src/hooks/queryBuilder/useTag.ts
Normal file
53
frontend/src/hooks/queryBuilder/useTag.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
type IUseTag = {
|
||||
handleAddTag: (value: string) => void;
|
||||
handleClearTag: (value: string) => void;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,
|
||||
): IUseTag => {
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
|
||||
/**
|
||||
* Adds a new tag to the tag list.
|
||||
* @param {string} value - The tag value to be added.
|
||||
*/
|
||||
const handleAddTag = useCallback(
|
||||
(value: string): void => {
|
||||
if (
|
||||
(value && key && isValidTag) ||
|
||||
isFreeText ||
|
||||
isExistsNotExistsOperator(value)
|
||||
) {
|
||||
setTags((prevTags) => [...prevTags, value]);
|
||||
handleSearch('');
|
||||
}
|
||||
},
|
||||
[key, isValidTag, isFreeText, handleSearch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Removes a tag from the tag list.
|
||||
* @param {string} value - The tag value to be removed.
|
||||
*/
|
||||
const handleClearTag = useCallback((value: string): void => {
|
||||
setTags((prevTags) => prevTags.filter((v) => v !== value));
|
||||
}, []);
|
||||
|
||||
return { handleAddTag, handleClearTag, tags };
|
||||
};
|
35
frontend/src/hooks/queryBuilder/useTagValidation.ts
Normal file
35
frontend/src/hooks/queryBuilder/useTagValidation.ts
Normal file
@ -0,0 +1,35 @@
|
||||
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';
|
||||
|
||||
type ITagValidation = {
|
||||
isValidTag: boolean;
|
||||
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 { isExist, isValidOperator, isMulti, isFreeText } = 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 { isValidTag, isExist, isValidOperator, isMulti, isFreeText };
|
||||
};
|
@ -1,7 +1,9 @@
|
||||
// ** Helpers
|
||||
// ** Constants
|
||||
import { initialQueryBuilderFormValues } from 'constants/queryBuilder';
|
||||
import { mapOfOperators } from 'constants/queryBuilder';
|
||||
import {
|
||||
initialQueryBuilderFormValues,
|
||||
mapOfOperators,
|
||||
} from 'constants/queryBuilder';
|
||||
import {
|
||||
createNewQueryName,
|
||||
MAX_QUERIES,
|
||||
|
4
frontend/src/utils/checkStringEndsWithSpace.ts
Normal file
4
frontend/src/utils/checkStringEndsWithSpace.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const checkStringEndsWithSpace = (str: string): boolean => {
|
||||
const endSpace = / $/;
|
||||
return endSpace.test(str);
|
||||
};
|
1
frontend/src/utils/getCountOfSpace.ts
Normal file
1
frontend/src/utils/getCountOfSpace.ts
Normal file
@ -0,0 +1 @@
|
||||
export const getCountOfSpace = (s: string): number => s.split(' ').length - 1;
|
9
frontend/src/utils/getSearchParams.ts
Normal file
9
frontend/src/utils/getSearchParams.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const getSearchParams = (newParams: {
|
||||
[key: string]: string;
|
||||
}): URLSearchParams => {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(newParams).forEach(([key, value]) => {
|
||||
params.set(key, value);
|
||||
});
|
||||
return params;
|
||||
};
|
12
frontend/src/utils/separateSearchValue.ts
Normal file
12
frontend/src/utils/separateSearchValue.ts
Normal file
@ -0,0 +1,12 @@
|
||||
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