fix: issue of new query builder v3 (#2697)

* fix: position of where caluse is changed for metrics

* fix: by default enabled for logs & traces

* fix: to many api call for key on search

* fix: make chip on enter for exists/nexists

* fix: flickering issue on selection of option

* fix: text change

* fix: orderby payload issue

* fix: removed replace logic

* fix: responsive qb & disabled issue

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Ankit Nayan <ankit@signoz.io>
This commit is contained in:
Chintan Sudani 2023-05-18 12:55:18 +05:30 committed by GitHub
parent a0c320e47e
commit ca3ff04f7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 206 additions and 148 deletions

View File

@ -14,11 +14,11 @@ import {
GroupByFilter,
HavingFilter,
OperatorsSelect,
OrderByFilter,
ReduceToFilter,
} from 'container/QueryBuilder/filters';
import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryFilter';
import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter';
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryOperations';
@ -233,6 +233,25 @@ export const Query = memo(function Query({
<FilterLabel label={transformToUpperCase(query.dataSource)} />
)}
</Col>
{isMetricsDataSource && (
<Col span={12}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<OperatorsSelect
value={query.aggregateOperator}
onChange={handleChangeOperator}
operators={operators}
/>
</Col>
<Col flex="1 1 12.5rem">
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
/>
</Col>
</Row>
</Col>
)}
<Col flex="1 1 20rem">
<Row gutter={[11, 5]}>
{isMetricsDataSource && (
@ -247,25 +266,26 @@ export const Query = memo(function Query({
</Col>
</Row>
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<OperatorsSelect
value={query.aggregateOperator}
onChange={handleChangeOperator}
operators={operators}
/>
</Col>
<Col flex="1 1 12.5rem">
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
/>
</Col>
</Row>
</Col>
<Col span={11} offset={2}>
{!isMetricsDataSource && (
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<OperatorsSelect
value={query.aggregateOperator}
onChange={handleChangeOperator}
operators={operators}
/>
</Col>
<Col flex="1 1 12.5rem">
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
/>
</Col>
</Row>
</Col>
)}
<Col span={11} offset={isMetricsDataSource ? 0 : 2}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel

View File

@ -4,6 +4,7 @@ import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { selectStyle } from '../QueryBuilderSearch/config';
@ -41,11 +42,16 @@ function AggregateEveryFilter({
}
};
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
return (
<Input
type="text"
placeholder="Enter in seconds"
disabled={!query.aggregateAttribute.key}
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
style={selectStyle}
defaultValue={query.stepInterval ?? stepInterval}
onChange={(event): void => onChange(Number(event.target.value))}

View File

@ -14,6 +14,7 @@ import {
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Having, HavingForm } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
// ** Types
@ -214,6 +215,11 @@ export function HavingFilter({
setLocalValues(transformHavingToStringValue(having));
}, [having]);
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
return (
<Select
autoClearSearchValue={false}
@ -223,7 +229,7 @@ export function HavingFilter({
tagRender={tagRender}
value={localValues}
data-testid="havingSelect"
disabled={!query.aggregateAttribute.key}
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
style={{ width: '100%' }}
notFoundContent={currentFormValue.value.length === 0 ? undefined : null}
placeholder="Count(operation) > 5"

View File

@ -1,6 +1,7 @@
import { InputNumber } from 'antd';
import React from 'react';
import React, { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { selectStyle } from '../QueryBuilderSearch/config';
@ -20,12 +21,17 @@ function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
}
};
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
return (
<InputNumber
min={1}
type="number"
defaultValue={query.limit ?? 1}
disabled={!query.aggregateAttribute.key}
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
style={selectStyle}
onChange={onChange}
onKeyDown={handleKeyDown}

View File

@ -1,9 +1,11 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
OrderByPayload,
} from 'types/api/queryBuilder/queryBuilderData';
export type OrderByFilterProps = {
query: IBuilderQuery;
onChange: (values: BaseAutocompleteData[]) => void;
onChange: (values: OrderByPayload[]) => void;
};
export type OrderByFilterValue = {

View File

@ -1,25 +1,28 @@
import { Select, Spin } from 'antd';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import * as Papa from 'papaparse';
import React, { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { MetricAggregateOperator } from 'types/common/queryBuilder';
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { selectStyle } from '../QueryBuilderSearch/config';
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
import { OrderByFilterProps } from './OrderByFilter.interfaces';
import {
OrderByFilterProps,
OrderByFilterValue,
} from './OrderByFilter.interfaces';
import { getLabelFromValue, mapLabelValuePairs } from './utils';
checkIfKeyPresent,
getLabelFromValue,
mapLabelValuePairs,
orderByValueDelimiter,
} from './utils';
export function OrderByFilter({
query,
onChange,
}: OrderByFilterProps): JSX.Element {
const [searchText, setSearchText] = useState<string>('');
const [selectedValue, setSelectedValue] = useState<OrderByFilterValue[]>([]);
const [selectedValue, setSelectedValue] = useState<string[]>([]);
const { data, isFetching } = useQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
@ -41,14 +44,9 @@ export function OrderByFilter({
const noAggregationOptions = useMemo(
() =>
data?.payload?.attributeKeys
? mapLabelValuePairs(data?.payload?.attributeKeys)
.flat()
.filter(
(option) =>
!getLabelFromValue(selectedValue).includes(option.label.split(' ')[0]),
)
? mapLabelValuePairs(data?.payload?.attributeKeys).flat()
: [],
[data?.payload?.attributeKeys, selectedValue],
[data?.payload?.attributeKeys],
);
const aggregationOptions = useMemo(
@ -58,78 +56,56 @@ export function OrderByFilter({
.concat([
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}asc`,
},
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}desc`,
},
])
.filter(
(option) =>
!getLabelFromValue(selectedValue).includes(option.label.split(' ')[0]),
),
[
query.aggregateAttribute.key,
query.aggregateOperator,
query.groupBy,
selectedValue,
],
]),
[query.aggregateAttribute.key, query.aggregateOperator, query.groupBy],
);
const optionsData = useMemo(
() =>
const optionsData = useMemo(() => {
const options =
query.aggregateOperator === MetricAggregateOperator.NOOP
? noAggregationOptions
: aggregationOptions,
[aggregationOptions, noAggregationOptions, query.aggregateOperator],
);
: aggregationOptions;
return options.filter(
(option) =>
!getLabelFromValue(selectedValue).includes(
getRemoveOrderFromValue(option.value),
),
);
}, [
aggregationOptions,
noAggregationOptions,
query.aggregateOperator,
selectedValue,
]);
const handleChange = (values: OrderByFilterValue[]): void => {
const handleChange = (values: string[]): void => {
setSelectedValue(values);
const orderByValues: BaseAutocompleteData[] = values?.map((item) => {
const iterationArray = data?.payload?.attributeKeys || query.orderBy;
const existingOrderValues = iterationArray.find(
(group) => group.key === item.value,
);
if (existingOrderValues) {
return existingOrderValues;
const orderByValues: OrderByPayload[] = values.map((item) => {
const match = Papa.parse(item, { delimiter: '|' });
if (match) {
const [columnName, order] = match.data.flat() as string[];
return {
columnName: checkIfKeyPresent(columnName, query.aggregateAttribute.key)
? '#SIGNOZ_VALUE'
: columnName,
order,
};
}
return {
isColumn: null,
key: item.value,
dataType: null,
type: null,
columnName: item,
order: '',
};
});
onChange(orderByValues);
};
const values: OrderByFilterValue[] = useMemo(
() =>
query.orderBy
.filter((order) =>
query.groupBy.find(
(group) =>
order.key.includes(group.key) ||
order.key.includes(query.aggregateOperator),
),
)
.map((item) => ({
label: transformStringWithPrefix({
str: item.key,
prefix: item.type || '',
condition: !item.isColumn,
}),
key: item.key,
value: item.key,
disabled: undefined,
title: undefined,
})),
[query.aggregateOperator, query.groupBy, query.orderBy],
);
const isDisabledSelect = useMemo(
() =>
!query.aggregateAttribute.key ||
@ -137,18 +113,21 @@ export function OrderByFilter({
[query.aggregateAttribute.key, query.aggregateOperator],
);
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
return (
<Select
mode="tags"
style={selectStyle}
onSearch={handleSearchKeys}
showSearch
disabled={isDisabledSelect}
disabled={isMetricsDataSource && isDisabledSelect}
showArrow={false}
filterOption={false}
options={optionsData}
labelInValue
value={values}
notFoundContent={isFetching ? <Spin size="small" /> : null}
onChange={handleChange}
/>

View File

@ -1,8 +1,9 @@
import { IOption } from 'hooks/useResourceAttribute/types';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import * as Papa from 'papaparse';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OrderByFilterValue } from './OrderByFilter.interfaces';
export const orderByValueDelimiter = '|';
export function mapLabelValuePairs(
arr: BaseAutocompleteData[],
@ -17,16 +18,27 @@ export function mapLabelValuePairs(
return [
{
label: `${label} asc`,
value: `${value} asc`,
value: `${value}${orderByValueDelimiter}asc`,
},
{
label: `${label} desc`,
value: `${value} desc`,
value: `${value}${orderByValueDelimiter}desc`,
},
];
});
}
export function getLabelFromValue(arr: OrderByFilterValue[]): string[] {
return arr.map((value) => value.label.split(' ')[0]);
export function getLabelFromValue(arr: string[]): string[] {
return arr.flat().map((item) => {
const match = Papa.parse(item, { delimiter: orderByValueDelimiter });
if (match) {
const [key] = match.data as string[];
return key[0];
}
return item;
});
}
export function checkIfKeyPresent(str: string, valueToCheck: string): boolean {
return new RegExp(`\\(${valueToCheck}\\)`).test(str);
}

View File

@ -1 +1 @@
export const selectStyle = { width: '100%', minWidth: '10rem' };
export const selectStyle = { width: '100%', minWidth: '7.7rem' };

View File

@ -88,15 +88,15 @@ function QueryBuilderSearch({
if (isExistsNotExistsOperator(searchValue)) handleKeyDown(event);
};
const isMatricsDataSource = useMemo(
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
const queryTags = useMemo(() => {
if (!query.aggregateAttribute.key && isMatricsDataSource) return [];
if (!query.aggregateAttribute.key && isMetricsDataSource) return [];
return tags;
}, [isMatricsDataSource, query.aggregateAttribute.key, tags]);
}, [isMetricsDataSource, query.aggregateAttribute.key, tags]);
useEffect(() => {
const initialTagFilters: TagFilter = { items: [], op: 'AND' };
@ -135,7 +135,7 @@ function QueryBuilderSearch({
placeholder="Search Filter"
value={queryTags}
searchValue={searchValue}
disabled={isMatricsDataSource && !query.aggregateAttribute.key}
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
style={selectStyle}
onSearch={handleSearch}
onChange={onChangeHandler}

View File

@ -2,6 +2,8 @@ import { OPERATORS } from 'constants/queryBuilder';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as Papa from 'papaparse';
import { orderByValueDelimiter } from '../OrderByFilter/utils';
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 {
@ -39,12 +41,13 @@ export function getTagToken(tag: string): ITagToken {
export function isExistsNotExistsOperator(value: string): boolean {
const { tagOperator } = getTagToken(value);
return (
tagOperator === OPERATORS.NOT_EXISTS || tagOperator === OPERATORS.EXISTS
tagOperator?.trim() === OPERATORS.NOT_EXISTS ||
tagOperator?.trim() === OPERATORS.EXISTS
);
}
export function getRemovePrefixFromKey(tag: string): string {
return tag?.replace(/^(tag_|resource_)/, '');
return tag?.replace(/^(tag_|resource_)/, '').trim();
}
export function getOperatorValue(op: string): string {
@ -106,3 +109,12 @@ export function replaceStringWithMaxLength(
export function checkCommaInValue(str: string): string {
return str.includes(',') ? `"${str}"` : str;
}
export function getRemoveOrderFromValue(tag: string): string {
const match = Papa.parse(tag, { delimiter: orderByValueDelimiter });
if (match) {
const [key] = match.data.flat() as string[];
return key;
}
return tag;
}

View File

@ -2,4 +2,5 @@ export { AggregatorFilter } from './AggregatorFilter';
export { GroupByFilter } from './GroupByFilter';
export { HavingFilter } from './HavingFilter';
export { OperatorsSelect } from './OperatorsSelect';
export { OrderByFilter } from './OrderByFilter';
export { ReduceToFilter } from './ReduceToFilter';

View File

@ -73,11 +73,9 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
return replaceStringWithMaxLength(prev, data as string[], value);
});
}
if (!isMulti && isValidTag && !isExistsNotExistsOperator(value)) {
handleAddTag(value);
}
if (!isMulti && isValidTag && isExistsNotExistsOperator(value)) {
handleAddTag(value);
if (!isMulti) {
if (isExistsNotExistsOperator(value)) handleAddTag(value);
if (isValidTag && !isExistsNotExistsOperator(value)) handleAddTag(value);
}
},
[handleAddTag, isMulti, isValidTag],

View File

@ -6,6 +6,7 @@ import {
getTagToken,
isInNInOperator,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import debounce from 'lodash-es/debounce';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useDebounce } from 'react-use';
@ -34,6 +35,25 @@ export const useFetchKeysAndValues = (
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
const [results, setResults] = useState<string[]>([]);
const searchParams = useMemo(
() =>
debounce(
() => [
searchKey,
query.dataSource,
query.aggregateOperator,
query.aggregateAttribute.key,
],
300,
),
[
query.aggregateAttribute.key,
query.aggregateOperator,
query.dataSource,
searchKey,
],
);
const isQueryEnabled = useMemo(
() =>
query.dataSource === DataSource.METRICS
@ -49,13 +69,7 @@ export const useFetchKeysAndValues = (
);
const { data, isFetching, status } = useQuery(
[
QueryBuilderKeys.GET_ATTRIBUTE_KEY,
searchKey,
query.dataSource,
query.aggregateOperator,
query.aggregateAttribute.key,
],
[QueryBuilderKeys.GET_ATTRIBUTE_KEY, searchParams()],
async () =>
getAggregateKeys({
searchText: searchKey,

View File

@ -52,41 +52,39 @@ export const useOptions = (
);
useEffect(() => {
let newOptions: Option[] = [];
if (!key) {
setOptions(
searchValue
? [
{ label: `${searchValue} `, value: `${searchValue} ` },
...getOptionsFromKeys(keys),
]
: getOptionsFromKeys(keys),
);
newOptions = searchValue
? [
{ label: `${searchValue} `, value: `${searchValue} ` },
...getOptionsFromKeys(keys),
]
: getOptionsFromKeys(keys);
} else if (key && !operator) {
setOptions(
operators?.map((operator) => ({
value: `${key} ${operator} `,
label: `${key} ${operator} `,
})),
);
newOptions = operators?.map((operator) => ({
value: `${key} ${operator} `,
label: `${key} ${operator} `,
}));
} else if (key && operator) {
if (isMulti) {
setOptions(
results.map((item) => ({
label: checkCommaInValue(String(item)),
value: String(item),
})),
);
newOptions = results.map((item) => ({
label: checkCommaInValue(String(item)),
value: String(item),
}));
} else if (isExist) {
setOptions([]);
newOptions = [];
} else if (isValidOperator) {
const hasAllResults = results.every((value) => result.includes(value));
const values = getKeyOpValue(results);
const options = hasAllResults
newOptions = hasAllResults
? [{ label: searchValue, value: searchValue }]
: [{ label: searchValue, value: searchValue }, ...values];
setOptions(options);
}
}
if (newOptions.length > 0) {
setOptions(newOptions);
}
}, [
getKeyOpValue,
getOptionsFromKeys,

View File

@ -42,7 +42,6 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
...query,
aggregateOperator: value,
having: [],
orderBy: [],
limit: null,
filters: { items: [], op: 'AND' },
...(shouldResetAggregateAttribute

View File

@ -33,6 +33,11 @@ export type HavingForm = Omit<Having, 'value'> & {
value: string[];
};
export type OrderByPayload = {
columnName: string;
order: string;
};
// Type for query builder
export type IBuilderQuery = {
queryName: string;
@ -46,7 +51,7 @@ export type IBuilderQuery = {
having: Having[];
limit: number | null;
stepInterval: number;
orderBy: BaseAutocompleteData[];
orderBy: OrderByPayload[];
reduceTo: ReduceOperators;
legend: string;
};