mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 21:09:03 +08:00
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:
parent
1e242b6d06
commit
821471f4ab
@ -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' },
|
||||
|
@ -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 = {
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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;
|
@ -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'>;
|
@ -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;
|
@ -0,0 +1,6 @@
|
||||
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export interface LimitFilterProps {
|
||||
onChange: (values: number | null) => void;
|
||||
formula: IBuilderFormula;
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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,
|
||||
};
|
||||
};
|
@ -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;
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const FILTERS = {
|
||||
export const ORDERBY_FILTERS = {
|
||||
ASC: 'asc',
|
||||
DESC: 'desc',
|
||||
};
|
||||
|
@ -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],
|
||||
|
@ -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;
|
||||
|
94
frontend/src/container/QueryBuilder/filters/utils.ts
Normal file
94
frontend/src/container/QueryBuilder/filters/utils.ts
Normal 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;
|
||||
}, []);
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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[];
|
||||
};
|
||||
|
@ -1 +1,5 @@
|
||||
export const popupContainer = (trigger: any): HTMLElement => trigger.parentNode;
|
||||
import { SelectProps } from 'antd';
|
||||
|
||||
export const popupContainer: SelectProps['getPopupContainer'] = (
|
||||
trigger,
|
||||
): HTMLElement => trigger.parentNode;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user