fix: custom where clause value (#3209)

* fix: custom where clause value

* fix: operations

* fix: return suggestions for body

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
Yevhen Shevchenko 2023-07-27 10:22:44 +03:00 committed by GitHub
parent 872759f579
commit 2c5c972801
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 143 additions and 38 deletions

View File

@ -1,5 +1,9 @@
import { Button } from 'antd'; import { Button } from 'antd';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import {
initialQueriesMap,
OPERATORS,
PANEL_TYPES,
} from 'constants/queryBuilder';
import ExplorerOrderBy from 'container/ExplorerOrderBy'; import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { QueryBuilder } from 'container/QueryBuilder'; import { QueryBuilder } from 'container/QueryBuilder';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces'; import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
@ -30,6 +34,10 @@ function LogExplorerQuerySection(): JSX.Element {
const isTable = panelTypes === PANEL_TYPES.TABLE; const isTable = panelTypes === PANEL_TYPES.TABLE;
const config: QueryBuilderProps['filterConfigs'] = { const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: isTable, isDisabled: true }, stepInterval: { isHidden: isTable, isDisabled: true },
filters: {
customKey: 'body',
customOp: OPERATORS.CONTAINS,
},
}; };
return config; return config;

View File

@ -1,10 +1,18 @@
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces'; import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces';
type FilterConfigs = {
[Key in keyof Omit<IBuilderQuery, 'filters'>]: {
isHidden: boolean;
isDisabled: boolean;
};
} & { filters: WhereClauseConfig };
export type QueryBuilderConfig = export type QueryBuilderConfig =
| { | {
queryVariant: 'static'; queryVariant: 'static';
@ -16,8 +24,6 @@ export type QueryBuilderProps = {
config?: QueryBuilderConfig; config?: QueryBuilderConfig;
panelType: PANEL_TYPES; panelType: PANEL_TYPES;
actions?: ReactNode; actions?: ReactNode;
filterConfigs?: Partial< filterConfigs?: Partial<FilterConfigs>;
Record<keyof IBuilderQuery, { isHidden: boolean; isDisabled: boolean }>
>;
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode }; queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
}; };

View File

@ -305,7 +305,11 @@ export const Query = memo(function Query({
</Col> </Col>
)} )}
<Col flex="1"> <Col flex="1">
<QueryBuilderSearch query={query} onChange={handleChangeTagFilters} /> <QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
</Col> </Col>
</Row> </Row>
</Col> </Col>

View File

@ -1,5 +1,8 @@
import { Select, Spin, Tag, Tooltip } from 'antd'; import { Select, Spin, Tag, Tooltip } from 'antd';
import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete'; import {
useAutoComplete,
WhereClauseConfig,
} from 'hooks/queryBuilder/useAutoComplete';
import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues'; import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues';
import { import {
KeyboardEvent, KeyboardEvent,
@ -31,6 +34,7 @@ import {
function QueryBuilderSearch({ function QueryBuilderSearch({
query, query,
onChange, onChange,
whereClauseConfig,
}: QueryBuilderSearchProps): JSX.Element { }: QueryBuilderSearchProps): JSX.Element {
const { const {
updateTag, updateTag,
@ -45,7 +49,7 @@ function QueryBuilderSearch({
isFetching, isFetching,
setSearchKey, setSearchKey,
searchKey, searchKey,
} = useAutoComplete(query); } = useAutoComplete(query, whereClauseConfig);
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues( const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
searchValue, searchValue,
@ -169,7 +173,7 @@ function QueryBuilderSearch({
notFoundContent={isFetching ? <Spin size="small" /> : null} notFoundContent={isFetching ? <Spin size="small" /> : null}
> >
{options.map((option) => ( {options.map((option) => (
<Select.Option key={option.label} value={option.label}> <Select.Option key={option.label} value={option.value}>
{option.label} {option.label}
{option.selected && <StyledCheckOutlined />} {option.selected && <StyledCheckOutlined />}
</Select.Option> </Select.Option>
@ -181,8 +185,13 @@ function QueryBuilderSearch({
interface QueryBuilderSearchProps { interface QueryBuilderSearchProps {
query: IBuilderQuery; query: IBuilderQuery;
onChange: (value: TagFilter) => void; onChange: (value: TagFilter) => void;
whereClauseConfig?: WhereClauseConfig;
} }
QueryBuilderSearch.defaultProps = {
whereClauseConfig: undefined,
};
export interface CustomTagProps { export interface CustomTagProps {
label: ReactNode; label: ReactNode;
value: string; value: string;

View File

@ -1,7 +1,6 @@
import { import {
getRemovePrefixFromKey, getRemovePrefixFromKey,
getTagToken, getTagToken,
isExistsNotExistsOperator,
replaceStringWithMaxLength, replaceStringWithMaxLength,
tagRegexp, tagRegexp,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
@ -16,7 +15,15 @@ import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator';
import { useTag } from './useTag'; import { useTag } from './useTag';
import { useTagValidation } from './useTagValidation'; import { useTagValidation } from './useTagValidation';
export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => { export type WhereClauseConfig = {
customKey: string;
customOp: string;
};
export const useAutoComplete = (
query: IBuilderQuery,
whereClauseConfig?: WhereClauseConfig,
): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>(''); const [searchValue, setSearchValue] = useState<string>('');
const [searchKey, setSearchKey] = useState<string>(''); const [searchKey, setSearchKey] = useState<string>('');
@ -40,11 +47,11 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
); );
const { handleAddTag, handleClearTag, tags, updateTag } = useTag( const { handleAddTag, handleClearTag, tags, updateTag } = useTag(
key,
isValidTag, isValidTag,
handleSearch, handleSearch,
query, query,
setSearchKey, setSearchKey,
whereClauseConfig,
); );
const handleSelect = useCallback( const handleSelect = useCallback(
@ -59,11 +66,10 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
}); });
} }
if (!isMulti) { if (!isMulti) {
if (isExistsNotExistsOperator(value)) handleAddTag(value); handleAddTag(value);
if (isValidTag && !isExistsNotExistsOperator(value)) handleAddTag(value);
} }
}, },
[handleAddTag, isMulti, isValidTag], [handleAddTag, isMulti],
); );
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
@ -102,6 +108,7 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
isExist, isExist,
results, results,
result, result,
whereClauseConfig,
); );
return { return {

View File

@ -7,8 +7,11 @@ import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { WhereClauseConfig } from './useAutoComplete';
import { useOperators } from './useOperators'; import { useOperators } from './useOperators';
export const WHERE_CLAUSE_CUSTOM_SUFFIX = '-custom';
export const useOptions = ( export const useOptions = (
key: string, key: string,
keys: BaseAutocompleteData[], keys: BaseAutocompleteData[],
@ -19,6 +22,7 @@ export const useOptions = (
isExist: boolean, isExist: boolean,
results: string[], results: string[],
result: string[], result: string[],
whereClauseConfig?: WhereClauseConfig,
): Option[] => { ): Option[] => {
const [options, setOptions] = useState<Option[]>([]); const [options, setOptions] = useState<Option[]>([]);
const operators = useOperators(key, keys); const operators = useOperators(key, keys);
@ -51,21 +55,64 @@ export const useOptions = (
[key, operator], [key, operator],
); );
const getOptionsWithValidOperator = useCallback(
(key: string, results: string[], searchValue: string) => {
const hasAllResults = results.every((value) => result.includes(value));
const values = getKeyOpValue(results);
return hasAllResults
? [
{
label: searchValue,
value: searchValue,
},
]
: [
{
label: searchValue,
value: searchValue,
},
...values,
];
},
[getKeyOpValue, result],
);
const getKeyOperatorOptions = useCallback(
(key: string) => {
const operatorsOptions = operators?.map((operator) => ({
value: `${key} ${operator} `,
label: `${key} ${operator} `,
}));
if (whereClauseConfig) {
return [
{
label: `${searchValue} `,
value: `${searchValue}${WHERE_CLAUSE_CUSTOM_SUFFIX}`,
},
...operatorsOptions,
];
}
return operatorsOptions;
},
[operators, searchValue, whereClauseConfig],
);
useEffect(() => { useEffect(() => {
let newOptions: Option[] = []; let newOptions: Option[] = [];
if (!key) { if (!key) {
newOptions = searchValue newOptions = searchValue
? [ ? [
{ label: `${searchValue} `, value: `${searchValue} ` }, {
label: `${searchValue} `,
value: `${searchValue} `,
},
...getOptionsFromKeys(keys), ...getOptionsFromKeys(keys),
] ]
: getOptionsFromKeys(keys); : getOptionsFromKeys(keys);
} else if (key && !operator) { } else if (key && !operator) {
newOptions = operators?.map((operator) => ({ newOptions = getKeyOperatorOptions(key);
value: `${key} ${operator} `,
label: `${key} ${operator} `,
}));
} else if (key && operator) { } else if (key && operator) {
if (isMulti) { if (isMulti) {
newOptions = results.map((item) => ({ newOptions = results.map((item) => ({
@ -75,17 +122,14 @@ export const useOptions = (
} else if (isExist) { } else if (isExist) {
newOptions = []; newOptions = [];
} else if (isValidOperator) { } else if (isValidOperator) {
const hasAllResults = results.every((value) => result.includes(value)); newOptions = getOptionsWithValidOperator(key, results, searchValue);
const values = getKeyOpValue(results);
newOptions = hasAllResults
? [{ label: searchValue, value: searchValue }]
: [{ label: searchValue, value: searchValue }, ...values];
} }
} }
if (newOptions.length > 0) { if (newOptions.length > 0) {
setOptions(newOptions); setOptions(newOptions);
} }
}, [ }, [
whereClauseConfig,
getKeyOpValue, getKeyOpValue,
getOptionsFromKeys, getOptionsFromKeys,
isExist, isExist,
@ -98,6 +142,8 @@ export const useOptions = (
result, result,
results, results,
searchValue, searchValue,
getKeyOperatorOptions,
getOptionsWithValidOperator,
]); ]);
return useMemo( return useMemo(

View File

@ -63,9 +63,18 @@ export const useQueryOperations: UseQueryOperations = ({
const getNewListOfAdditionalFilters = useCallback( const getNewListOfAdditionalFilters = useCallback(
(dataSource: DataSource): string[] => { (dataSource: DataSource): string[] => {
const additionalFiltersKeys: (keyof Pick<
IBuilderQuery,
'orderBy' | 'limit' | 'having' | 'stepInterval'
>)[] = ['having', 'limit', 'orderBy', 'stepInterval'];
const result: string[] = mapOfFilters[dataSource].reduce<string[]>( const result: string[] = mapOfFilters[dataSource].reduce<string[]>(
(acc, item) => { (acc, item) => {
if (filterConfigs && filterConfigs[item.field]?.isHidden) { if (
filterConfigs &&
filterConfigs[item.field as typeof additionalFiltersKeys[number]]
?.isHidden
) {
return acc; return acc;
} }

View File

@ -15,17 +15,14 @@ export const useSetCurrentKeyAndOperator = (
let key = ''; let key = '';
let operator = ''; let operator = '';
let result: string[] = []; let result: string[] = [];
const { tagKey, tagOperator, tagValue } = getTagToken(value);
if (value) { const isSuggestKey = keys?.some(
const { tagKey, tagOperator, tagValue } = getTagToken(value); (el) => el?.key === getRemovePrefixFromKey(tagKey),
const isSuggestKey = keys?.some( );
(el) => el?.key === getRemovePrefixFromKey(tagKey), if (isSuggestKey || keys.length === 0) {
); key = tagKey || '';
if (isSuggestKey || keys.length === 0) { operator = tagOperator || '';
key = tagKey || ''; result = tagValue || [];
operator = tagOperator || '';
result = tagValue || [];
}
} }
return [key, operator, result]; return [key, operator, result];

View File

@ -1,5 +1,6 @@
import { import {
getOperatorFromValue, getOperatorFromValue,
getTagToken,
isExistsNotExistsOperator, isExistsNotExistsOperator,
isInNInOperator, isInNInOperator,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
@ -8,6 +9,8 @@ import * as Papa from 'papaparse';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { WhereClauseConfig } from './useAutoComplete';
type IUseTag = { type IUseTag = {
handleAddTag: (value: string) => void; handleAddTag: (value: string) => void;
handleClearTag: (value: string) => void; handleClearTag: (value: string) => void;
@ -24,11 +27,11 @@ type IUseTag = {
*/ */
export const useTag = ( export const useTag = (
key: string,
isValidTag: boolean, isValidTag: boolean,
handleSearch: (value: string) => void, handleSearch: (value: string) => void,
query: IBuilderQuery, query: IBuilderQuery,
setSearchKey: (value: string) => void, setSearchKey: (value: string) => void,
whereClauseConfig?: WhereClauseConfig,
): IUseTag => { ): IUseTag => {
const initTagsData = useMemo( const initTagsData = useMemo(
() => () =>
@ -57,15 +60,31 @@ export const useTag = (
* Adds a new tag to the tag list. * Adds a new tag to the tag list.
* @param {string} value - The tag value to be added. * @param {string} value - The tag value to be added.
*/ */
const handleAddTag = useCallback( const handleAddTag = useCallback(
(value: string): void => { (value: string): void => {
const { tagKey } = getTagToken(value);
const [key, id] = tagKey.split('-');
if (id === 'custom') {
const customValue = whereClauseConfig
? `${whereClauseConfig.customKey} ${whereClauseConfig.customOp} ${key}`
: '';
setTags((prevTags) =>
prevTags.includes(customValue) ? prevTags : [...prevTags, customValue],
);
handleSearch('');
setSearchKey('');
return;
}
if ((value && key && isValidTag) || isExistsNotExistsOperator(value)) { if ((value && key && isValidTag) || isExistsNotExistsOperator(value)) {
setTags((prevTags) => [...prevTags, value]); setTags((prevTags) => [...prevTags, value]);
handleSearch(''); handleSearch('');
setSearchKey(''); setSearchKey('');
} }
}, },
[key, isValidTag, handleSearch, setSearchKey], [whereClauseConfig, isValidTag, handleSearch, setSearchKey],
); );
/** /**