feat(query-builder): add limit, order by and having clause to formula (#3623)

* feat: query builder formula is updated

* feat: formula is updated for having and limit

* feat: orderBy is updated

* feat: formula is added

* chore: add query-service support for formula limit and order by

* feat: enable more filters is displayed when all data source is metrics

* chore: feedback is updated

* chore: feedback is updated

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
Co-authored-by: Rajat Dabade <rajat@signoz.io>
This commit is contained in:
Palash Gupta 2023-09-27 12:04:47 +00:00 committed by GitHub
parent 1e242b6d06
commit 821471f4ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 916 additions and 211 deletions

View File

@ -74,7 +74,7 @@ export const mapOfOperators = {
traces: tracesAggregateOperatorOptions,
};
export const mapOfFilters: Record<DataSource, QueryAdditionalFilter[]> = {
export const mapOfQueryFilters: Record<DataSource, QueryAdditionalFilter[]> = {
metrics: [
// eslint-disable-next-line sonarjs/no-duplicate-string
{ text: 'Aggregation interval', field: 'stepInterval' },
@ -94,6 +94,24 @@ export const mapOfFilters: Record<DataSource, QueryAdditionalFilter[]> = {
],
};
const commonFormulaFilters: QueryAdditionalFilter[] = [
{
text: 'Having',
field: 'having',
},
{ text: 'Order by', field: 'orderBy' },
{ text: 'Limit', field: 'limit' },
];
export const mapOfFormulaToFilters: Record<
DataSource,
QueryAdditionalFilter[]
> = {
metrics: commonFormulaFilters,
logs: commonFormulaFilters,
traces: commonFormulaFilters,
};
export const REDUCE_TO_VALUES: SelectOption<ReduceOperators, string>[] = [
{ value: 'last', label: 'Latest of values in timeframe' },
{ value: 'sum', label: 'Sum of values in timeframe' },

View File

@ -2,7 +2,7 @@ import {
initialQueriesMap,
initialQueryBuilderFormValuesMap,
} from 'constants/queryBuilder';
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import {
BaseAutocompleteData,
DataTypes,
@ -14,7 +14,7 @@ export const defaultLiveQueryDataConfig: Partial<IBuilderQuery> = {
aggregateOperator: LogsAggregatorOperator.NOOP,
disabled: true,
pageSize: 10,
orderBy: [{ columnName: 'timestamp', order: FILTERS.DESC }],
orderBy: [{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC }],
};
type GetDefaultCompositeQueryParams = {

View File

@ -1,5 +1,5 @@
import { Button, Typography } from 'antd';
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ShowButtonWrapper } from './styles';
@ -19,7 +19,7 @@ function ShowButton({
return (
<ShowButtonWrapper>
<Typography>
Showing 10 lines {order === FILTERS.ASC ? 'after' : 'before'} match
Showing 10 lines {order === ORDERBY_FILTERS.ASC ? 'after' : 'before'} match
</Typography>
<Button
size="small"

View File

@ -1,7 +1,7 @@
import RawLogView from 'components/Logs/RawLogView';
import Spinner from 'components/Spinner';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
@ -87,7 +87,7 @@ function LogsContextList({
timestamp: item.timestamp,
}));
if (order === FILTERS.ASC) {
if (order === ORDERBY_FILTERS.ASC) {
const reversedCurrentLogs = currentLogs.reverse();
setLogs((prevLogs) => [...reversedCurrentLogs, ...prevLogs]);
} else {
@ -111,7 +111,7 @@ function LogsContextList({
const handleShowNextLines = useCallback(() => {
if (isDisabledFetch) return;
const log = order === FILTERS.ASC ? firstLog : lastLog;
const log = order === ORDERBY_FILTERS.ASC ? firstLog : lastLog;
const newRequestData = getRequestData({
stagedQueryData: currentStagedQueryData,
@ -167,7 +167,7 @@ function LogsContextList({
return (
<>
{order === FILTERS.ASC && (
{order === ORDERBY_FILTERS.ASC && (
<ShowButton
isLoading={isFetching}
isDisabled={isDisabledFetch}
@ -186,11 +186,11 @@ function LogsContextList({
initialTopMostItemIndex={0}
data={logs}
itemContent={getItemContent}
followOutput={order === FILTERS.DESC}
followOutput={order === ORDERBY_FILTERS.DESC}
/>
</ListContainer>
{order === FILTERS.DESC && (
{order === ORDERBY_FILTERS.DESC && (
<ShowButton
isLoading={isFetching}
isDisabled={isDisabledFetch}

View File

@ -3,7 +3,7 @@ import { Typography } from 'antd';
import Modal from 'antd/es/modal/Modal';
import RawLogView from 'components/Logs/RawLogView';
import LogsContextList from 'container/LogsContextList';
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { memo, useCallback, useState } from 'react';
@ -87,7 +87,7 @@ function LogsExplorerContext({
/>
)}
<LogsContextList
order={FILTERS.ASC}
order={ORDERBY_FILTERS.ASC}
filters={filters}
isEdit={isEdit}
log={log}
@ -103,7 +103,7 @@ function LogsExplorerContext({
/>
</LogContainer>
<LogsContextList
order={FILTERS.DESC}
order={ORDERBY_FILTERS.DESC}
filters={filters}
isEdit={isEdit}
log={log}

View File

@ -5,6 +5,7 @@ import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
// ** Constants
import { memo, useEffect, useMemo } from 'react';
import { DataSource } from 'types/common/queryBuilder';
// ** Components
import { Formula, Query } from './components';
@ -79,11 +80,27 @@ export const QueryBuilder = memo(function QueryBuilder({
/>
</Col>
))}
{currentQuery.builder.queryFormulas.map((formula, index) => (
{currentQuery.builder.queryFormulas.map((formula, index) => {
const isAllMetricDataSource = currentQuery.builder.queryData.every(
(query) => query.dataSource === DataSource.METRICS,
);
const query =
currentQuery.builder.queryData[index] ||
currentQuery.builder.queryData[0];
return (
<Col key={formula.queryName} span={24}>
<Formula formula={formula} index={index} />
<Formula
filterConfigs={filterConfigs}
query={query}
isAdditionalFilterEnable={isAllMetricDataSource}
formula={formula}
index={index}
/>
</Col>
))}
);
})}
</Row>
</Col>

View File

@ -21,13 +21,13 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
setIsOpenedFilters((prevState) => !prevState);
};
const filtersTexts: ReactNode = listOfAdditionalFilter.map((str, index) => {
const filtersTexts: ReactNode = listOfAdditionalFilter?.map((str, index) => {
const isNextLast = index + 1 === listOfAdditionalFilter.length - 1;
if (index === listOfAdditionalFilter.length - 1) {
return (
<Fragment key={str}>
{listOfAdditionalFilter.length > 1 && 'and'}{' '}
{listOfAdditionalFilter?.length > 1 && 'and'}{' '}
<StyledLink>{str.toUpperCase()}</StyledLink>
</Fragment>
);

View File

@ -1,3 +1,13 @@
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
export type FormulaProps = { formula: IBuilderFormula; index: number };
export type FormulaProps = {
formula: IBuilderFormula;
index: number;
query: IBuilderQuery;
filterConfigs: Partial<QueryBuilderProps['filterConfigs']>;
isAdditionalFilterEnable: boolean;
};

View File

@ -1,22 +1,45 @@
import { Col, Input } from 'antd';
import { Col, Input, Row } from 'antd';
// ** Components
import { ListItemWrapper, ListMarker } from 'container/QueryBuilder/components';
import {
FilterLabel,
ListItemWrapper,
ListMarker,
} from 'container/QueryBuilder/components';
import HavingFilter from 'container/QueryBuilder/filters/Formula/Having/HavingFilter';
import LimitFilter from 'container/QueryBuilder/filters/Formula/Limit/Limit';
import OrderByFilter from 'container/QueryBuilder/filters/Formula/OrderBy/OrderByFilter';
// ** Hooks
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ChangeEvent, useCallback } from 'react';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChangeEvent, useCallback, useMemo } from 'react';
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
import { AdditionalFiltersToggler } from '../AdditionalFiltersToggler';
// ** Types
import { FormulaProps } from './Formula.interfaces';
const { TextArea } = Input;
export function Formula({ index, formula }: FormulaProps): JSX.Element {
export function Formula({
index,
formula,
filterConfigs,
query,
isAdditionalFilterEnable,
}: FormulaProps): JSX.Element {
const {
removeQueryBuilderEntityByIndex,
handleSetFormulaData,
} = useQueryBuilder();
const {
listOfAdditionalFormulaFilters,
handleChangeFormulaData,
} = useQueryOperations({
index,
query,
filterConfigs,
formula,
});
const handleDelete = useCallback(() => {
removeQueryBuilderEntityByIndex('queryFormulas', index);
}, [index, removeQueryBuilderEntityByIndex]);
@ -43,6 +66,75 @@ export function Formula({ index, formula }: FormulaProps): JSX.Element {
[index, formula, handleSetFormulaData],
);
const handleChangeLimit = useCallback(
(value: IBuilderFormula['limit']) => {
handleChangeFormulaData('limit', value);
},
[handleChangeFormulaData],
);
const handleChangeHavingFilter = useCallback(
(value: IBuilderFormula['having']) => {
handleChangeFormulaData('having', value);
},
[handleChangeFormulaData],
);
const handleChangeOrderByFilter = useCallback(
(value: IBuilderFormula['orderBy']) => {
handleChangeFormulaData('orderBy', value);
},
[handleChangeFormulaData],
);
const renderAdditionalFilters = useMemo(
() => (
<>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Limit" />
</Col>
<Col flex="1 1 12.5rem">
<LimitFilter formula={formula} onChange={handleChangeLimit} />
</Col>
</Row>
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="HAVING" />
</Col>
<Col flex="1 1 12.5rem">
<HavingFilter formula={formula} onChange={handleChangeHavingFilter} />
</Col>
</Row>
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Order by" />
</Col>
<Col flex="1 1 12.5rem">
<OrderByFilter
query={query}
formula={formula}
onChange={handleChangeOrderByFilter}
/>
</Col>
</Row>
</Col>
</>
),
[
formula,
handleChangeHavingFilter,
handleChangeLimit,
handleChangeOrderByFilter,
query,
],
);
return (
<ListItemWrapper onDelete={handleDelete}>
<Col span={24}>
@ -54,7 +146,7 @@ export function Formula({ index, formula }: FormulaProps): JSX.Element {
/>
</Col>
<Col span={24}>
<TextArea
<Input.TextArea
name="expression"
onChange={handleChange}
size="middle"
@ -71,6 +163,17 @@ export function Formula({ index, formula }: FormulaProps): JSX.Element {
addonBefore="Legend Format"
/>
</Col>
{isAdditionalFilterEnable && (
<Col span={24}>
<AdditionalFiltersToggler
listOfAdditionalFilter={listOfAdditionalFormulaFilters}
>
<Row gutter={[0, 11]} justify="space-between">
{renderAdditionalFilters}
</Row>
</AdditionalFiltersToggler>
</Col>
)}
</ListItemWrapper>
);
}

View File

@ -21,7 +21,7 @@ import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryF
import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryOperations';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
// ** Hooks
import { ChangeEvent, memo, ReactNode, useCallback } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';

View File

@ -0,0 +1,198 @@
import { Select } from 'antd';
import { HAVING_OPERATORS, initialHavingValues } from 'constants/queryBuilder';
import { HavingFilterTag } from 'container/QueryBuilder/components';
import { useTagValidation } from 'hooks/queryBuilder/useTagValidation';
import {
transformFromStringToHaving,
transformHavingToStringValue,
} from 'lib/query/transformQueryBuilderData';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Having, HavingForm } from 'types/api/queryBuilder/queryBuilderData';
import { SelectOption } from 'types/common/select';
import { popupContainer } from 'utils/selectPopupContainer';
import { getHavingObject, isValidHavingValue } from '../../utils';
import { HavingFilterProps, HavingTagRenderProps } from './types';
function HavingFilter({ formula, onChange }: HavingFilterProps): JSX.Element {
const { having } = formula;
const [searchText, setSearchText] = useState<string>('');
const [localValues, setLocalValues] = useState<string[]>([]);
const [currentFormValue, setCurrentFormValue] = useState<HavingForm>(
initialHavingValues,
);
const [options, setOptions] = useState<SelectOption<string, string>[]>([]);
const { isMulti } = useTagValidation(
currentFormValue.op,
currentFormValue.value,
);
const columnName = formula.expression.toUpperCase();
const aggregatorOptions: SelectOption<string, string>[] = useMemo(
() => [{ label: columnName, value: columnName }],
[columnName],
);
const handleUpdateTag = useCallback(
(value: string) => {
const filteredValues = localValues.filter(
(currentValue) => currentValue !== value,
);
const having: Having[] = filteredValues.map(transformFromStringToHaving);
onChange(having);
setSearchText(value);
},
[localValues, onChange],
);
const generateOptions = useCallback(
(currentString: string) => {
const [aggregator = '', op = '', ...restValue] = currentString.split(' ');
let newOptions: SelectOption<string, string>[] = [];
const isAggregatorExist = columnName
.toLowerCase()
.includes(currentString.toLowerCase());
const isAggregatorChosen = aggregator === columnName;
if (isAggregatorExist || aggregator === '') {
newOptions = aggregatorOptions;
}
if ((isAggregatorChosen && op === '') || op) {
const filteredOperators = HAVING_OPERATORS.filter((num) =>
num.toLowerCase().includes(op.toLowerCase()),
);
newOptions = filteredOperators.map((opt) => ({
label: `${columnName} ${opt} ${restValue && restValue.join(' ')}`,
value: `${columnName} ${opt} ${restValue && restValue.join(' ')}`,
}));
}
setOptions(newOptions);
},
[aggregatorOptions, columnName],
);
const parseSearchText = useCallback(
(text: string) => {
const { columnName, op, value } = getHavingObject(text);
setCurrentFormValue({ columnName, op, value });
generateOptions(text);
},
[generateOptions],
);
const tagRender = ({
label,
value,
closable,
disabled,
onClose,
}: HavingTagRenderProps): JSX.Element => {
const handleClose = (): void => {
onClose();
setSearchText('');
};
return (
<HavingFilterTag
label={label}
value={value}
closable={closable}
disabled={disabled}
onClose={handleClose}
onUpdate={handleUpdateTag}
/>
);
};
const handleSearch = (search: string): void => {
const trimmedSearch = search.replace(/\s\s+/g, ' ').trimStart();
const currentSearch = isMulti
? trimmedSearch
: trimmedSearch.split(' ').slice(0, 3).join(' ');
const isValidSearch = isValidHavingValue(currentSearch);
if (isValidSearch) {
setSearchText(currentSearch);
}
};
useEffect(() => {
setLocalValues(transformHavingToStringValue(having || []));
}, [having]);
useEffect(() => {
parseSearchText(searchText);
}, [searchText, parseSearchText]);
const resetChanges = (): void => {
setSearchText('');
setCurrentFormValue(initialHavingValues);
setOptions(aggregatorOptions);
};
const handleDeselect = (value: string): void => {
const result = localValues.filter((item) => item !== value);
const having: Having[] = result.map(transformFromStringToHaving);
onChange(having);
resetChanges();
};
const handleSelect = (currentValue: string): void => {
const { columnName, op, value } = getHavingObject(currentValue);
const isCompletedValue = value.every((item) => !!item);
const isClearSearch = isCompletedValue && columnName && op;
setSearchText(isClearSearch ? '' : currentValue);
};
const handleChange = (values: string[]): void => {
const having: Having[] = values.map(transformFromStringToHaving);
const isSelectable =
currentFormValue.value.length > 0 &&
currentFormValue.value.every((value) => !!value);
if (isSelectable) {
onChange(having);
resetChanges();
}
};
return (
<Select
getPopupContainer={popupContainer}
autoClearSearchValue={false}
mode="multiple"
onSearch={handleSearch}
searchValue={searchText}
data-testid="havingSelectFormula"
placeholder="Count(operation) > 5"
style={{ width: '100%' }}
tagRender={tagRender}
onDeselect={handleDeselect}
onSelect={handleSelect}
onChange={handleChange}
value={localValues}
>
{options.map((opt) => (
<Select.Option key={opt.value} value={opt.value} title="havingOption">
{opt.label}
</Select.Option>
))}
</Select>
);
}
export default HavingFilter;

View File

@ -0,0 +1,12 @@
import { HavingFilterTagProps } from 'container/QueryBuilder/components/HavingFilterTag/HavingFilterTag.interfaces';
import {
Having,
IBuilderFormula,
} from 'types/api/queryBuilder/queryBuilderData';
export type HavingFilterProps = {
formula: IBuilderFormula;
onChange: (having: Having[]) => void;
};
export type HavingTagRenderProps = Omit<HavingFilterTagProps, 'onUpdate'>;

View File

@ -0,0 +1,20 @@
import { InputNumber } from 'antd';
import { selectStyle } from '../../QueryBuilderSearch/config';
import { handleKeyDownLimitFilter } from '../../utils';
import { LimitFilterProps } from './types';
function LimitFilter({ onChange, formula }: LimitFilterProps): JSX.Element {
return (
<InputNumber
min={1}
type="number"
value={formula.limit}
style={selectStyle}
onChange={onChange}
onKeyDown={handleKeyDownLimitFilter}
/>
);
}
export default LimitFilter;

View File

@ -0,0 +1,6 @@
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
export interface LimitFilterProps {
onChange: (values: number | null) => void;
formula: IBuilderFormula;
}

View File

@ -0,0 +1,84 @@
import { Select, Spin } from 'antd';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useMemo } from 'react';
import { MetricAggregateOperator } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { selectStyle } from '../../QueryBuilderSearch/config';
import { OrderByProps } from './types';
import { useOrderByFormulaFilter } from './useOrderByFormulaFilter';
function OrderByFilter({
formula,
onChange,
query,
}: OrderByProps): JSX.Element {
const {
debouncedSearchText,
createOptions,
aggregationOptions,
handleChange,
handleSearchKeys,
selectedValue,
generateOptions,
} = useOrderByFormulaFilter({
query,
onChange,
formula,
});
const { data, isFetching } = useGetAggregateKeys(
{
aggregateAttribute: query.aggregateAttribute.key,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
searchText: debouncedSearchText,
},
{
enabled: !!query.aggregateAttribute.key,
keepPreviousData: true,
},
);
const optionsData = useMemo(() => {
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
const groupByOptions = createOptions(query.groupBy);
const options =
query.aggregateOperator === MetricAggregateOperator.NOOP
? keyOptions
: [...groupByOptions, ...aggregationOptions];
return generateOptions(options);
}, [
aggregationOptions,
createOptions,
data?.payload?.attributeKeys,
generateOptions,
query.aggregateOperator,
query.groupBy,
]);
const isDisabledSelect =
!query.aggregateAttribute.key ||
query.aggregateOperator === MetricAggregateOperator.NOOP;
return (
<Select
getPopupContainer={popupContainer}
mode="tags"
style={selectStyle}
onSearch={handleSearchKeys}
showSearch
disabled={isDisabledSelect}
showArrow={false}
value={selectedValue}
labelInValue
filterOption={false}
options={optionsData}
notFoundContent={isFetching ? <Spin size="small" /> : null}
onChange={handleChange}
/>
);
}
export default OrderByFilter;

View File

@ -0,0 +1,12 @@
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
export interface OrderByProps {
formula: IBuilderFormula;
query: IBuilderQuery;
onChange: (value: IBuilderFormula['orderBy']) => void;
}
export type IOrderByFormulaFilterProps = OrderByProps;

View File

@ -0,0 +1,127 @@
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import useDebounce from 'hooks/useDebounce';
import { IOption } from 'hooks/useResourceAttribute/types';
import isEqual from 'lodash-es/isEqual';
import uniqWith from 'lodash-es/uniqWith';
import { parse } from 'papaparse';
import { useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
import { ORDERBY_FILTERS } from '../../OrderByFilter/config';
import { SIGNOZ_VALUE } from '../../OrderByFilter/constants';
import { UseOrderByFilterResult } from '../../OrderByFilter/useOrderByFilter';
import {
getLabelFromValue,
mapLabelValuePairs,
orderByValueDelimiter,
} from '../../OrderByFilter/utils';
import { getRemoveOrderFromValue } from '../../QueryBuilderSearch/utils';
import { getUniqueOrderByValues, getValidOrderByResult } from '../../utils';
import { IOrderByFormulaFilterProps } from './types';
import { transformToOrderByStringValuesByFormula } from './utils';
export const useOrderByFormulaFilter = ({
onChange,
formula,
}: IOrderByFormulaFilterProps): UseOrderByFilterResult => {
const [searchText, setSearchText] = useState<string>('');
const debouncedSearchText = useDebounce(searchText, DEBOUNCE_DELAY);
const handleSearchKeys = (searchText: string): void =>
setSearchText(searchText);
const handleChange = (values: IOption[]): void => {
const validResult = getValidOrderByResult(values);
const result = getUniqueOrderByValues(validResult);
const orderByValues: OrderByPayload[] = result.map((item) => {
const match = parse(item.value, { delimiter: orderByValueDelimiter });
if (!match) {
return {
columnName: item.value,
order: ORDERBY_FILTERS.ASC,
};
}
const [columnName, order] = match.data.flat() as string[];
const columnNameValue =
columnName === SIGNOZ_VALUE ? SIGNOZ_VALUE : columnName;
const orderValue = order ?? ORDERBY_FILTERS.ASC;
return {
columnName: columnNameValue,
order: orderValue,
};
});
setSearchText('');
onChange(orderByValues);
};
const aggregationOptions = [
{
label: `${formula.expression} ${ORDERBY_FILTERS.ASC}`,
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
},
{
label: `${formula.expression} ${ORDERBY_FILTERS.DESC}`,
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${ORDERBY_FILTERS.DESC}`,
},
];
const selectedValue = transformToOrderByStringValuesByFormula(formula);
const createOptions = (data: BaseAutocompleteData[]): IOption[] =>
mapLabelValuePairs(data).flat();
const customValue: IOption[] = useMemo(() => {
if (!searchText) return [];
return [
{
label: `${searchText} ${ORDERBY_FILTERS.ASC}`,
value: `${searchText}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
},
{
label: `${searchText} ${ORDERBY_FILTERS.DESC}`,
value: `${searchText}${orderByValueDelimiter}${ORDERBY_FILTERS.DESC}`,
},
];
}, [searchText]);
const generateOptions = (options: IOption[]): IOption[] => {
const currentCustomValue = options.find(
(keyOption) =>
getRemoveOrderFromValue(keyOption.value) === debouncedSearchText,
)
? []
: customValue;
const result = [...currentCustomValue, ...options];
const uniqResult = uniqWith(result, isEqual);
return uniqResult.filter(
(option) =>
!getLabelFromValue(selectedValue).includes(
getRemoveOrderFromValue(option.value),
),
);
};
return {
searchText,
debouncedSearchText,
selectedValue,
aggregationOptions,
createOptions,
handleChange,
handleSearchKeys,
generateOptions,
};
};

View File

@ -0,0 +1,26 @@
import { IOption } from 'hooks/useResourceAttribute/types';
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
import { SIGNOZ_VALUE } from '../../OrderByFilter/constants';
import { orderByValueDelimiter } from '../../OrderByFilter/utils';
export const transformToOrderByStringValuesByFormula = (
formula: IBuilderFormula,
): IOption[] => {
const prepareSelectedValue: IOption[] =
formula?.orderBy?.map((item) => {
if (item.columnName === SIGNOZ_VALUE) {
return {
label: `${formula.expression} ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
};
}
return {
label: `${item.columnName} ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
};
}) || [];
return prepareSelectedValue;
};

View File

@ -1,7 +1,6 @@
import { Select } from 'antd';
// ** Constants
import { HAVING_OPERATORS, initialHavingValues } from 'constants/queryBuilder';
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
import { HavingFilterTag } from 'container/QueryBuilder/components';
import { HavingTagRenderProps } from 'container/QueryBuilder/components/HavingFilterTag/HavingFilterTag.interfaces';
// ** Hooks
@ -18,11 +17,10 @@ import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
import { popupContainer } from 'utils/selectPopupContainer';
import { getHavingObject, isValidHavingValue } from '../utils';
// ** Types
import { HavingFilterProps } from './HavingFilter.interfaces';
const { Option } = Select;
export function HavingFilter({
query,
onChange,
@ -60,13 +58,6 @@ export function HavingFilter({
[columnName],
);
const getHavingObject = useCallback((currentSearch: string): HavingForm => {
const textArr = currentSearch.split(' ');
const [columnName = '', op = '', ...value] = textArr;
return { columnName, op, value };
}, []);
const generateOptions = useCallback(
(search: string): void => {
const [aggregator = '', op = '', ...restValue] = search.split(' ');
@ -98,19 +89,6 @@ export function HavingFilter({
[columnName, aggregatorOptions],
);
const isValidHavingValue = useCallback(
(search: string): boolean => {
const values = getHavingObject(search).value.join(' ');
if (values) {
return HAVING_FILTER_REGEXP.test(values);
}
return true;
},
[getHavingObject],
);
const handleSearch = useCallback(
(search: string): void => {
const trimmedSearch = search.replace(/\s\s+/g, ' ').trimStart();
@ -125,7 +103,7 @@ export function HavingFilter({
setSearchText(currentSearch);
}
},
[isMulti, isValidHavingValue],
[isMulti],
);
const resetChanges = useCallback((): void => {
@ -200,7 +178,7 @@ export function HavingFilter({
generateOptions(text);
},
[generateOptions, getHavingObject],
[generateOptions],
);
const handleDeselect = (value: string): void => {
@ -218,10 +196,7 @@ export function HavingFilter({
setLocalValues(transformHavingToStringValue(having));
}, [having]);
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
return (
<Select
@ -242,9 +217,9 @@ export function HavingFilter({
onSelect={handleSelect}
>
{options.map((opt) => (
<Option key={opt.value} value={opt.value} title="havingOption">
<Select.Option key={opt.value} value={opt.value} title="havingOption">
{opt.label}
</Option>
</Select.Option>
))}
</Select>
);

View File

@ -1,30 +1,12 @@
import { InputNumber } from 'antd';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { selectStyle } from '../QueryBuilderSearch/config';
import { handleKeyDownLimitFilter } from '../utils';
function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
const handleKeyDown = (event: {
keyCode: number;
which: number;
preventDefault: () => void;
}): void => {
const keyCode = event.keyCode || event.which;
const isBackspace = keyCode === 8;
const isNumeric =
(keyCode >= 48 && keyCode <= 57) || (keyCode >= 96 && keyCode <= 105);
if (!isNumeric && !isBackspace) {
event.preventDefault();
}
};
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key;
@ -36,7 +18,7 @@ function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
style={selectStyle}
disabled={isDisabled}
onChange={onChange}
onKeyDown={handleKeyDown}
onKeyDown={handleKeyDownLimitFilter}
/>
);
}

View File

@ -9,9 +9,9 @@ export type OrderByFilterProps = {
};
export type OrderByFilterValue = {
disabled: boolean | undefined;
disabled?: boolean;
key: string;
label: string;
title: string | undefined;
title?: string;
value: string;
};

View File

@ -53,17 +53,11 @@ export function OrderByFilter({
query.groupBy,
]);
const isDisabledSelect = useMemo(
() =>
const isDisabledSelect =
!query.aggregateAttribute.key ||
query.aggregateOperator === MetricAggregateOperator.NOOP,
[query.aggregateAttribute.key, query.aggregateOperator],
);
query.aggregateOperator === MetricAggregateOperator.NOOP;
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
return (
<Select

View File

@ -1,4 +1,4 @@
export const FILTERS = {
export const ORDERBY_FILTERS = {
ASC: 'asc',
DESC: 'desc',
};

View File

@ -8,18 +8,18 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
import { FILTERS } from './config';
import { getUniqueOrderByValues, getValidOrderByResult } from '../utils';
import { ORDERBY_FILTERS } from './config';
import { SIGNOZ_VALUE } from './constants';
import { OrderByFilterProps } from './OrderByFilter.interfaces';
import {
getLabelFromValue,
mapLabelValuePairs,
orderByValueDelimiter,
splitOrderByFromString,
transformToOrderByStringValues,
} from './utils';
type UseOrderByFilterResult = {
export type UseOrderByFilterResult = {
searchText: string;
debouncedSearchText: string;
selectedValue: IOption[];
@ -43,43 +43,17 @@ export const useOrderByFilter = ({
[],
);
const getUniqValues = useCallback((values: IOption[]): IOption[] => {
const modifiedValues = values.map((item) => {
const match = parse(item.value, { delimiter: orderByValueDelimiter });
if (!match) return { label: item.label, value: item.value };
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
const [_, order] = match.data.flat() as string[];
if (order)
return {
label: item.label,
value: item.value,
};
return {
label: `${item.value} ${FILTERS.ASC}`,
value: `${item.value}${orderByValueDelimiter}${FILTERS.ASC}`,
};
});
return uniqWith(
modifiedValues,
(current, next) =>
getRemoveOrderFromValue(current.value) ===
getRemoveOrderFromValue(next.value),
);
}, []);
const customValue: IOption[] = useMemo(() => {
if (!searchText) return [];
return [
{
label: `${searchText} ${FILTERS.ASC}`,
value: `${searchText}${orderByValueDelimiter}${FILTERS.ASC}`,
label: `${searchText} ${ORDERBY_FILTERS.ASC}`,
value: `${searchText}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
},
{
label: `${searchText} ${FILTERS.DESC}`,
value: `${searchText}${orderByValueDelimiter}${FILTERS.DESC}`,
label: `${searchText} ${ORDERBY_FILTERS.DESC}`,
value: `${searchText}${orderByValueDelimiter}${ORDERBY_FILTERS.DESC}`,
},
];
}, [searchText]);
@ -111,34 +85,9 @@ export const useOrderByFilter = ({
[customValue, debouncedSearchText, selectedValue],
);
const getValidResult = useCallback(
(result: IOption[]): IOption[] =>
result.reduce<IOption[]>((acc, item) => {
if (item.value === FILTERS.ASC || item.value === FILTERS.DESC) return acc;
if (item.value.includes(FILTERS.ASC) || item.value.includes(FILTERS.DESC)) {
const splittedOrderBy = splitOrderByFromString(item.value);
if (splittedOrderBy) {
acc.push({
label: `${splittedOrderBy.columnName} ${splittedOrderBy.order}`,
value: `${splittedOrderBy.columnName}${orderByValueDelimiter}${splittedOrderBy.order}`,
});
return acc;
}
}
acc.push(item);
return acc;
}, []),
[],
);
const handleChange = (values: IOption[]): void => {
const validResult = getValidResult(values);
const result = getUniqValues(validResult);
const validResult = getValidOrderByResult(values);
const result = getUniqueOrderByValues(validResult);
const orderByValues: OrderByPayload[] = result.map((item) => {
const match = parse(item.value, { delimiter: orderByValueDelimiter });
@ -175,12 +124,12 @@ export const useOrderByFilter = ({
const aggregationOptions = useMemo(
() => [
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.ASC}`,
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${FILTERS.ASC}`,
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${ORDERBY_FILTERS.ASC}`,
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
},
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.DESC}`,
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${FILTERS.DESC}`,
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${ORDERBY_FILTERS.DESC}`,
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${ORDERBY_FILTERS.DESC}`,
},
],
[query],

View File

@ -6,7 +6,7 @@ import {
OrderByPayload,
} from 'types/api/queryBuilder/queryBuilderData';
import { FILTERS } from './config';
import { ORDERBY_FILTERS } from './config';
import { SIGNOZ_VALUE } from './constants';
export const orderByValueDelimiter = '|';
@ -38,12 +38,12 @@ export function mapLabelValuePairs(
const value = item.key;
return [
{
label: `${value} ${FILTERS.ASC}`,
value: `${value}${orderByValueDelimiter}${FILTERS.ASC}`,
label: `${value} ${ORDERBY_FILTERS.ASC}`,
value: `${value}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
},
{
label: `${value} ${FILTERS.DESC}`,
value: `${value}${orderByValueDelimiter}${FILTERS.DESC}`,
label: `${value} ${ORDERBY_FILTERS.DESC}`,
value: `${value}${orderByValueDelimiter}${ORDERBY_FILTERS.DESC}`,
},
];
});
@ -68,7 +68,7 @@ export function checkIfKeyPresent(str: string, valueToCheck: string): boolean {
export function splitOrderByFromString(str: string): OrderByPayload | null {
const splittedStr = str.split(' ');
const order = splittedStr.pop() || FILTERS.ASC;
const order = splittedStr.pop() || ORDERBY_FILTERS.ASC;
const columnName = splittedStr.join(' ');
if (!columnName) return null;

View File

@ -0,0 +1,94 @@
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
import { IOption } from 'hooks/useResourceAttribute/types';
import uniqWith from 'lodash-es/unionWith';
import { parse } from 'papaparse';
import { HavingForm } from 'types/api/queryBuilder/queryBuilderData';
import { ORDERBY_FILTERS } from './OrderByFilter/config';
import {
orderByValueDelimiter,
splitOrderByFromString,
} from './OrderByFilter/utils';
import { getRemoveOrderFromValue } from './QueryBuilderSearch/utils';
export const handleKeyDownLimitFilter: React.KeyboardEventHandler<HTMLInputElement> = (
event,
): void => {
const keyCode = event.keyCode || event.which;
const isBackspace = keyCode === 8;
const isNumeric =
(keyCode >= 48 && keyCode <= 57) || (keyCode >= 96 && keyCode <= 105);
if (!isNumeric && !isBackspace) {
event.preventDefault();
}
};
export const getHavingObject = (currentSearch: string): HavingForm => {
const textArr = currentSearch.split(' ');
const [columnName = '', op = '', ...value] = textArr;
return { columnName, op, value };
};
export const isValidHavingValue = (search: string): boolean => {
const values = getHavingObject(search).value.join(' ');
if (values) {
return HAVING_FILTER_REGEXP.test(values);
}
return true;
};
export const getUniqueOrderByValues = (values: IOption[]): IOption[] => {
const modifiedValues = values.map((item) => {
const match = parse(item.value, { delimiter: orderByValueDelimiter });
if (!match) return { label: item.label, value: item.value };
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
const [_, order] = match.data.flat() as string[];
if (order)
return {
label: item.label,
value: item.value,
};
return {
label: `${item.value} ${ORDERBY_FILTERS.ASC}`,
value: `${item.value}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
};
});
return uniqWith(
modifiedValues,
(current, next) =>
getRemoveOrderFromValue(current.value) ===
getRemoveOrderFromValue(next.value),
);
};
export const getValidOrderByResult = (result: IOption[]): IOption[] =>
result.reduce<IOption[]>((acc, item) => {
if (item.value === ORDERBY_FILTERS.ASC || item.value === ORDERBY_FILTERS.DESC)
return acc;
if (
item.value.includes(ORDERBY_FILTERS.ASC) ||
item.value.includes(ORDERBY_FILTERS.DESC)
) {
const splittedOrderBy = splitOrderByFromString(item.value);
if (splittedOrderBy) {
acc.push({
label: `${splittedOrderBy.columnName} ${splittedOrderBy.order}`,
value: `${splittedOrderBy.columnName}${orderByValueDelimiter}${splittedOrderBy.order}`,
});
return acc;
}
}
acc.push(item);
return acc;
}, []);

View File

@ -1,7 +1,8 @@
import {
initialAutocompleteData,
initialQueryBuilderFormValuesMap,
mapOfFilters,
mapOfFormulaToFilters,
mapOfQueryFilters,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@ -9,8 +10,12 @@ import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperato
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
import { useCallback, useEffect, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
import {
HandleChangeFormulaData,
HandleChangeQueryData,
UseQueryOperations,
} from 'types/common/operations.types';
@ -21,54 +26,31 @@ export const useQueryOperations: UseQueryOperations = ({
query,
index,
filterConfigs,
formula,
}) => {
const {
handleSetQueryData,
handleSetFormulaData,
removeQueryBuilderEntityByIndex,
panelType,
initialDataSource,
currentQuery,
} = useQueryBuilder();
const [operators, setOperators] = useState<SelectOption<string, string>[]>([]);
const [listOfAdditionalFilters, setListOfAdditionalFilters] = useState<
string[]
>([]);
const { dataSource, aggregateOperator } = query;
const handleChangeOperator = useCallback(
(value: string): void => {
const aggregateDataType: BaseAutocompleteData['dataType'] =
query.aggregateAttribute.dataType;
const typeOfValue = findDataTypeOfOperator(value);
const shouldResetAggregateAttribute =
(aggregateDataType === 'string' || aggregateDataType === 'bool') &&
typeOfValue === 'number';
const newQuery: IBuilderQuery = {
...query,
aggregateOperator: value,
having: [],
limit: null,
...(shouldResetAggregateAttribute
? { aggregateAttribute: initialAutocompleteData }
: {}),
};
handleSetQueryData(index, newQuery);
},
[index, query, handleSetQueryData],
);
const getNewListOfAdditionalFilters = useCallback(
(dataSource: DataSource): string[] => {
(dataSource: DataSource, isQuery: boolean): string[] => {
const additionalFiltersKeys: (keyof Pick<
IBuilderQuery,
'orderBy' | 'limit' | 'having' | 'stepInterval'
>)[] = ['having', 'limit', 'orderBy', 'stepInterval'];
const result: string[] = mapOfFilters[dataSource].reduce<string[]>(
const mapsOfFilters = isQuery ? mapOfQueryFilters : mapOfFormulaToFilters;
const result: string[] = mapsOfFilters[dataSource]?.reduce<string[]>(
(acc, item) => {
if (
filterConfigs &&
@ -91,6 +73,41 @@ export const useQueryOperations: UseQueryOperations = ({
[filterConfigs],
);
const [listOfAdditionalFilters, setListOfAdditionalFilters] = useState<
string[]
>(getNewListOfAdditionalFilters(dataSource, true));
const [
listOfAdditionalFormulaFilters,
setListOfAdditionalFormulaFilters,
] = useState<string[]>(getNewListOfAdditionalFilters(dataSource, false));
const handleChangeOperator = useCallback(
(value: string): void => {
const aggregateDataType: BaseAutocompleteData['dataType'] =
query.aggregateAttribute.dataType;
const typeOfValue = findDataTypeOfOperator(value);
const shouldResetAggregateAttribute =
(aggregateDataType === 'string' || aggregateDataType === 'bool') &&
typeOfValue === 'number';
const newQuery: IBuilderQuery = {
...query,
aggregateOperator: value,
having: [],
limit: null,
...(shouldResetAggregateAttribute
? { aggregateAttribute: initialAutocompleteData }
: {}),
};
handleSetQueryData(index, newQuery);
},
[index, query, handleSetQueryData],
);
const handleChangeAggregatorAttribute = useCallback(
(value: BaseAutocompleteData): void => {
const newQuery: IBuilderQuery = {
@ -148,6 +165,18 @@ export const useQueryOperations: UseQueryOperations = ({
[query, index, handleSetQueryData],
);
const handleChangeFormulaData: HandleChangeFormulaData = useCallback(
(key, value) => {
const newFormula: IBuilderFormula = {
...(formula || ({} as IBuilderFormula)),
[key]: value,
};
handleSetFormulaData(index, newFormula);
},
[formula, handleSetFormulaData, index],
);
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
const isTracePanelType = panelType === PANEL_TYPES.TRACE;
@ -166,11 +195,17 @@ export const useQueryOperations: UseQueryOperations = ({
}, [dataSource, initialDataSource, panelType, operators]);
useEffect(() => {
const additionalFilters = getNewListOfAdditionalFilters(dataSource);
const additionalFilters = getNewListOfAdditionalFilters(dataSource, true);
setListOfAdditionalFilters(additionalFilters);
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
useEffect(() => {
const additionalFilters = getNewListOfAdditionalFilters(dataSource, false);
setListOfAdditionalFormulaFilters(additionalFilters);
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
return {
isTracePanelType,
isMetricsDataSource,
@ -181,5 +216,7 @@ export const useQueryOperations: UseQueryOperations = ({
handleChangeDataSource,
handleDeleteQuery,
handleChangeQueryData,
listOfAdditionalFormulaFilters,
handleChangeFormulaData,
};
};

View File

@ -19,12 +19,12 @@ export const getOperatorsBySourceAndPanelType = ({
let operatorsByDataSource = mapOfOperators[dataSource];
if (panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE) {
operatorsByDataSource = operatorsByDataSource.filter(
operatorsByDataSource = operatorsByDataSource?.filter(
(operator) => operator.value === StringOperators.NOOP,
);
}
if (panelType === PANEL_TYPES.TABLE && dataSource === DataSource.METRICS) {
operatorsByDataSource = operatorsByDataSource.filter(
operatorsByDataSource = operatorsByDataSource?.filter(
(operator) =>
operator.value !== MetricAggregateOperator.NOOP &&
operator.value !== MetricAggregateOperator.RATE,
@ -35,7 +35,7 @@ export const getOperatorsBySourceAndPanelType = ({
panelType !== PANEL_TYPES.LIST &&
panelType !== PANEL_TYPES.TRACE
) {
operatorsByDataSource = operatorsByDataSource.filter(
operatorsByDataSource = operatorsByDataSource?.filter(
(operator) => operator.value !== StringOperators.NOOP,
);
}

View File

@ -1,4 +1,5 @@
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { OPERATORS } from 'constants/queryBuilder';
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
@ -51,7 +52,10 @@ export const getPaginationQueryData: SetupPaginationQueryData = ({
dataType: DataTypes.String,
isColumn: true,
},
op: orderByTimestamp.order === FILTERS.ASC ? '>' : '<',
op:
orderByTimestamp.order === ORDERBY_FILTERS.ASC
? OPERATORS['>']
: OPERATORS['<'],
value: listItemId,
},
...updatedFilters.items,

View File

@ -13,8 +13,11 @@ export interface IBuilderFormula {
expression: string;
disabled: boolean;
queryName: string;
dataSource?: DataSource;
legend: string;
limit?: number | null;
having?: Having[];
stepInterval?: number;
orderBy?: OrderByPayload[];
}
export interface TagFilterItem {

View File

@ -1,13 +1,18 @@
import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from './select';
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
Pick<QueryBuilderProps, 'filterConfigs'>;
Pick<QueryBuilderProps, 'filterConfigs'> & {
formula?: IBuilderFormula;
};
export type HandleChangeQueryData = <
Key extends keyof IBuilderQuery,
@ -17,6 +22,14 @@ export type HandleChangeQueryData = <
value: Value,
) => void;
export type HandleChangeFormulaData = <
Key extends keyof IBuilderFormula,
Value extends IBuilderFormula[Key]
>(
key: Key,
value: Value,
) => void;
export type UseQueryOperations = (
params: UseQueryOperationsParams,
) => {
@ -29,4 +42,6 @@ export type UseQueryOperations = (
handleChangeDataSource: (newSource: DataSource) => void;
handleDeleteQuery: () => void;
handleChangeQueryData: HandleChangeQueryData;
handleChangeFormulaData: HandleChangeFormulaData;
listOfAdditionalFormulaFilters: string[];
};

View File

@ -1 +1,5 @@
export const popupContainer = (trigger: any): HTMLElement => trigger.parentNode;
import { SelectProps } from 'antd';
export const popupContainer: SelectProps['getPopupContainer'] = (
trigger,
): HTMLElement => trigger.parentNode;

View File

@ -3051,7 +3051,9 @@ func applyMetricLimit(results []*v3.Result, queryRangeParams *v3.QueryRangeParam
for _, result := range results {
builderQueries := queryRangeParams.CompositeQuery.BuilderQueries
if builderQueries != nil && builderQueries[result.QueryName].DataSource == v3.DataSourceMetrics {
if builderQueries != nil && (builderQueries[result.QueryName].DataSource == v3.DataSourceMetrics ||
result.QueryName != builderQueries[result.QueryName].Expression) {
limit := builderQueries[result.QueryName].Limit
orderByList := builderQueries[result.QueryName].OrderBy

View File

@ -87,7 +87,12 @@ func unique(slice []string) []string {
}
// expressionToQuery constructs the query for the expression
func expressionToQuery(qp *v3.QueryRangeParamsV3, varToQuery map[string]string, expression *govaluate.EvaluableExpression) (string, error) {
func expressionToQuery(
qp *v3.QueryRangeParamsV3,
varToQuery map[string]string,
expression *govaluate.EvaluableExpression,
queryName string,
) (string, error) {
var formulaQuery string
variables := unique(expression.Vars())
@ -134,6 +139,14 @@ func expressionToQuery(qp *v3.QueryRangeParamsV3, varToQuery map[string]string,
prevVar = variable
}
formulaQuery = fmt.Sprintf("SELECT %s, %s as value FROM ", joinUsing, formula.ExpressionString()) + formulaSubQuery
if len(qp.CompositeQuery.BuilderQueries[queryName].Having) > 0 {
conditions := []string{}
for _, having := range qp.CompositeQuery.BuilderQueries[queryName].Having {
conditions = append(conditions, fmt.Sprintf("%s %s %v", "value", having.Operator, having.Value))
}
havingClause := " HAVING " + strings.Join(conditions, " AND ")
formulaQuery += havingClause
}
return formulaQuery, nil
}
@ -236,7 +249,7 @@ func (qb *QueryBuilder) PrepareQueries(params *v3.QueryRangeParamsV3, args ...in
if query.Expression != query.QueryName {
expression, _ := govaluate.NewEvaluableExpressionWithFunctions(query.Expression, EvalFuncs)
queryString, err := expressionToQuery(params, queries, expression)
queryString, err := expressionToQuery(params, queries, expression, query.QueryName)
if err != nil {
return nil, err
}