mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 20:16:00 +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,
|
traces: tracesAggregateOperatorOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapOfFilters: Record<DataSource, QueryAdditionalFilter[]> = {
|
export const mapOfQueryFilters: Record<DataSource, QueryAdditionalFilter[]> = {
|
||||||
metrics: [
|
metrics: [
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
{ text: 'Aggregation interval', field: 'stepInterval' },
|
{ 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>[] = [
|
export const REDUCE_TO_VALUES: SelectOption<ReduceOperators, string>[] = [
|
||||||
{ value: 'last', label: 'Latest of values in timeframe' },
|
{ value: 'last', label: 'Latest of values in timeframe' },
|
||||||
{ value: 'sum', label: 'Sum of values in timeframe' },
|
{ value: 'sum', label: 'Sum of values in timeframe' },
|
||||||
|
@ -2,7 +2,7 @@ import {
|
|||||||
initialQueriesMap,
|
initialQueriesMap,
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||||
import {
|
import {
|
||||||
BaseAutocompleteData,
|
BaseAutocompleteData,
|
||||||
DataTypes,
|
DataTypes,
|
||||||
@ -14,7 +14,7 @@ export const defaultLiveQueryDataConfig: Partial<IBuilderQuery> = {
|
|||||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
orderBy: [{ columnName: 'timestamp', order: FILTERS.DESC }],
|
orderBy: [{ columnName: 'timestamp', order: ORDERBY_FILTERS.DESC }],
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetDefaultCompositeQueryParams = {
|
type GetDefaultCompositeQueryParams = {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Button, Typography } from 'antd';
|
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';
|
import { ShowButtonWrapper } from './styles';
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ function ShowButton({
|
|||||||
return (
|
return (
|
||||||
<ShowButtonWrapper>
|
<ShowButtonWrapper>
|
||||||
<Typography>
|
<Typography>
|
||||||
Showing 10 lines {order === FILTERS.ASC ? 'after' : 'before'} match
|
Showing 10 lines {order === ORDERBY_FILTERS.ASC ? 'after' : 'before'} match
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
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 { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
@ -87,7 +87,7 @@ function LogsContextList({
|
|||||||
timestamp: item.timestamp,
|
timestamp: item.timestamp,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (order === FILTERS.ASC) {
|
if (order === ORDERBY_FILTERS.ASC) {
|
||||||
const reversedCurrentLogs = currentLogs.reverse();
|
const reversedCurrentLogs = currentLogs.reverse();
|
||||||
setLogs((prevLogs) => [...reversedCurrentLogs, ...prevLogs]);
|
setLogs((prevLogs) => [...reversedCurrentLogs, ...prevLogs]);
|
||||||
} else {
|
} else {
|
||||||
@ -111,7 +111,7 @@ function LogsContextList({
|
|||||||
const handleShowNextLines = useCallback(() => {
|
const handleShowNextLines = useCallback(() => {
|
||||||
if (isDisabledFetch) return;
|
if (isDisabledFetch) return;
|
||||||
|
|
||||||
const log = order === FILTERS.ASC ? firstLog : lastLog;
|
const log = order === ORDERBY_FILTERS.ASC ? firstLog : lastLog;
|
||||||
|
|
||||||
const newRequestData = getRequestData({
|
const newRequestData = getRequestData({
|
||||||
stagedQueryData: currentStagedQueryData,
|
stagedQueryData: currentStagedQueryData,
|
||||||
@ -167,7 +167,7 @@ function LogsContextList({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{order === FILTERS.ASC && (
|
{order === ORDERBY_FILTERS.ASC && (
|
||||||
<ShowButton
|
<ShowButton
|
||||||
isLoading={isFetching}
|
isLoading={isFetching}
|
||||||
isDisabled={isDisabledFetch}
|
isDisabled={isDisabledFetch}
|
||||||
@ -186,11 +186,11 @@ function LogsContextList({
|
|||||||
initialTopMostItemIndex={0}
|
initialTopMostItemIndex={0}
|
||||||
data={logs}
|
data={logs}
|
||||||
itemContent={getItemContent}
|
itemContent={getItemContent}
|
||||||
followOutput={order === FILTERS.DESC}
|
followOutput={order === ORDERBY_FILTERS.DESC}
|
||||||
/>
|
/>
|
||||||
</ListContainer>
|
</ListContainer>
|
||||||
|
|
||||||
{order === FILTERS.DESC && (
|
{order === ORDERBY_FILTERS.DESC && (
|
||||||
<ShowButton
|
<ShowButton
|
||||||
isLoading={isFetching}
|
isLoading={isFetching}
|
||||||
isDisabled={isDisabledFetch}
|
isDisabled={isDisabledFetch}
|
||||||
|
@ -3,7 +3,7 @@ import { Typography } from 'antd';
|
|||||||
import Modal from 'antd/es/modal/Modal';
|
import Modal from 'antd/es/modal/Modal';
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
import LogsContextList from 'container/LogsContextList';
|
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 QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { memo, useCallback, useState } from 'react';
|
import { memo, useCallback, useState } from 'react';
|
||||||
@ -87,7 +87,7 @@ function LogsExplorerContext({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LogsContextList
|
<LogsContextList
|
||||||
order={FILTERS.ASC}
|
order={ORDERBY_FILTERS.ASC}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
isEdit={isEdit}
|
isEdit={isEdit}
|
||||||
log={log}
|
log={log}
|
||||||
@ -103,7 +103,7 @@ function LogsExplorerContext({
|
|||||||
/>
|
/>
|
||||||
</LogContainer>
|
</LogContainer>
|
||||||
<LogsContextList
|
<LogsContextList
|
||||||
order={FILTERS.DESC}
|
order={ORDERBY_FILTERS.DESC}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
isEdit={isEdit}
|
isEdit={isEdit}
|
||||||
log={log}
|
log={log}
|
||||||
|
@ -5,6 +5,7 @@ import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder';
|
|||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
// ** Constants
|
// ** Constants
|
||||||
import { memo, useEffect, useMemo } from 'react';
|
import { memo, useEffect, useMemo } from 'react';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
// ** Components
|
// ** Components
|
||||||
import { Formula, Query } from './components';
|
import { Formula, Query } from './components';
|
||||||
@ -79,11 +80,27 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</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}>
|
<Col key={formula.queryName} span={24}>
|
||||||
<Formula formula={formula} index={index} />
|
<Formula
|
||||||
|
filterConfigs={filterConfigs}
|
||||||
|
query={query}
|
||||||
|
isAdditionalFilterEnable={isAllMetricDataSource}
|
||||||
|
formula={formula}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
@ -21,13 +21,13 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
|
|||||||
setIsOpenedFilters((prevState) => !prevState);
|
setIsOpenedFilters((prevState) => !prevState);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filtersTexts: ReactNode = listOfAdditionalFilter.map((str, index) => {
|
const filtersTexts: ReactNode = listOfAdditionalFilter?.map((str, index) => {
|
||||||
const isNextLast = index + 1 === listOfAdditionalFilter.length - 1;
|
const isNextLast = index + 1 === listOfAdditionalFilter.length - 1;
|
||||||
|
|
||||||
if (index === listOfAdditionalFilter.length - 1) {
|
if (index === listOfAdditionalFilter.length - 1) {
|
||||||
return (
|
return (
|
||||||
<Fragment key={str}>
|
<Fragment key={str}>
|
||||||
{listOfAdditionalFilter.length > 1 && 'and'}{' '}
|
{listOfAdditionalFilter?.length > 1 && 'and'}{' '}
|
||||||
<StyledLink>{str.toUpperCase()}</StyledLink>
|
<StyledLink>{str.toUpperCase()}</StyledLink>
|
||||||
</Fragment>
|
</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
|
// ** 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
|
// ** Hooks
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
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 { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { AdditionalFiltersToggler } from '../AdditionalFiltersToggler';
|
||||||
// ** Types
|
// ** Types
|
||||||
import { FormulaProps } from './Formula.interfaces';
|
import { FormulaProps } from './Formula.interfaces';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
export function Formula({
|
||||||
|
index,
|
||||||
export function Formula({ index, formula }: FormulaProps): JSX.Element {
|
formula,
|
||||||
|
filterConfigs,
|
||||||
|
query,
|
||||||
|
isAdditionalFilterEnable,
|
||||||
|
}: FormulaProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
removeQueryBuilderEntityByIndex,
|
removeQueryBuilderEntityByIndex,
|
||||||
handleSetFormulaData,
|
handleSetFormulaData,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const {
|
||||||
|
listOfAdditionalFormulaFilters,
|
||||||
|
handleChangeFormulaData,
|
||||||
|
} = useQueryOperations({
|
||||||
|
index,
|
||||||
|
query,
|
||||||
|
filterConfigs,
|
||||||
|
formula,
|
||||||
|
});
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
removeQueryBuilderEntityByIndex('queryFormulas', index);
|
removeQueryBuilderEntityByIndex('queryFormulas', index);
|
||||||
}, [index, removeQueryBuilderEntityByIndex]);
|
}, [index, removeQueryBuilderEntityByIndex]);
|
||||||
@ -43,6 +66,75 @@ export function Formula({ index, formula }: FormulaProps): JSX.Element {
|
|||||||
[index, formula, handleSetFormulaData],
|
[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 (
|
return (
|
||||||
<ListItemWrapper onDelete={handleDelete}>
|
<ListItemWrapper onDelete={handleDelete}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
@ -54,7 +146,7 @@ export function Formula({ index, formula }: FormulaProps): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<TextArea
|
<Input.TextArea
|
||||||
name="expression"
|
name="expression"
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
size="middle"
|
size="middle"
|
||||||
@ -71,6 +163,17 @@ export function Formula({ index, formula }: FormulaProps): JSX.Element {
|
|||||||
addonBefore="Legend Format"
|
addonBefore="Legend Format"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
{isAdditionalFilterEnable && (
|
||||||
|
<Col span={24}>
|
||||||
|
<AdditionalFiltersToggler
|
||||||
|
listOfAdditionalFilter={listOfAdditionalFormulaFilters}
|
||||||
|
>
|
||||||
|
<Row gutter={[0, 11]} justify="space-between">
|
||||||
|
{renderAdditionalFilters}
|
||||||
|
</Row>
|
||||||
|
</AdditionalFiltersToggler>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
</ListItemWrapper>
|
</ListItemWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryF
|
|||||||
import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter';
|
import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter';
|
||||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryOperations';
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
// ** Hooks
|
// ** Hooks
|
||||||
import { ChangeEvent, memo, ReactNode, useCallback } from 'react';
|
import { ChangeEvent, memo, ReactNode, useCallback } from 'react';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
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';
|
import { Select } from 'antd';
|
||||||
// ** Constants
|
// ** Constants
|
||||||
import { HAVING_OPERATORS, initialHavingValues } from 'constants/queryBuilder';
|
import { HAVING_OPERATORS, initialHavingValues } from 'constants/queryBuilder';
|
||||||
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
|
|
||||||
import { HavingFilterTag } from 'container/QueryBuilder/components';
|
import { HavingFilterTag } from 'container/QueryBuilder/components';
|
||||||
import { HavingTagRenderProps } from 'container/QueryBuilder/components/HavingFilterTag/HavingFilterTag.interfaces';
|
import { HavingTagRenderProps } from 'container/QueryBuilder/components/HavingFilterTag/HavingFilterTag.interfaces';
|
||||||
// ** Hooks
|
// ** Hooks
|
||||||
@ -18,11 +17,10 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
import { SelectOption } from 'types/common/select';
|
import { SelectOption } from 'types/common/select';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
|
import { getHavingObject, isValidHavingValue } from '../utils';
|
||||||
// ** Types
|
// ** Types
|
||||||
import { HavingFilterProps } from './HavingFilter.interfaces';
|
import { HavingFilterProps } from './HavingFilter.interfaces';
|
||||||
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
export function HavingFilter({
|
export function HavingFilter({
|
||||||
query,
|
query,
|
||||||
onChange,
|
onChange,
|
||||||
@ -60,13 +58,6 @@ export function HavingFilter({
|
|||||||
[columnName],
|
[columnName],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getHavingObject = useCallback((currentSearch: string): HavingForm => {
|
|
||||||
const textArr = currentSearch.split(' ');
|
|
||||||
const [columnName = '', op = '', ...value] = textArr;
|
|
||||||
|
|
||||||
return { columnName, op, value };
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const generateOptions = useCallback(
|
const generateOptions = useCallback(
|
||||||
(search: string): void => {
|
(search: string): void => {
|
||||||
const [aggregator = '', op = '', ...restValue] = search.split(' ');
|
const [aggregator = '', op = '', ...restValue] = search.split(' ');
|
||||||
@ -98,19 +89,6 @@ export function HavingFilter({
|
|||||||
[columnName, aggregatorOptions],
|
[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(
|
const handleSearch = useCallback(
|
||||||
(search: string): void => {
|
(search: string): void => {
|
||||||
const trimmedSearch = search.replace(/\s\s+/g, ' ').trimStart();
|
const trimmedSearch = search.replace(/\s\s+/g, ' ').trimStart();
|
||||||
@ -125,7 +103,7 @@ export function HavingFilter({
|
|||||||
setSearchText(currentSearch);
|
setSearchText(currentSearch);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isMulti, isValidHavingValue],
|
[isMulti],
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetChanges = useCallback((): void => {
|
const resetChanges = useCallback((): void => {
|
||||||
@ -200,7 +178,7 @@ export function HavingFilter({
|
|||||||
|
|
||||||
generateOptions(text);
|
generateOptions(text);
|
||||||
},
|
},
|
||||||
[generateOptions, getHavingObject],
|
[generateOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeselect = (value: string): void => {
|
const handleDeselect = (value: string): void => {
|
||||||
@ -218,10 +196,7 @@ export function HavingFilter({
|
|||||||
setLocalValues(transformHavingToStringValue(having));
|
setLocalValues(transformHavingToStringValue(having));
|
||||||
}, [having]);
|
}, [having]);
|
||||||
|
|
||||||
const isMetricsDataSource = useMemo(
|
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
|
||||||
() => query.dataSource === DataSource.METRICS,
|
|
||||||
[query.dataSource],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@ -242,9 +217,9 @@ export function HavingFilter({
|
|||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
>
|
>
|
||||||
{options.map((opt) => (
|
{options.map((opt) => (
|
||||||
<Option key={opt.value} value={opt.value} title="havingOption">
|
<Select.Option key={opt.value} value={opt.value} title="havingOption">
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
@ -1,30 +1,12 @@
|
|||||||
import { InputNumber } from 'antd';
|
import { InputNumber } from 'antd';
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
|
import { handleKeyDownLimitFilter } from '../utils';
|
||||||
|
|
||||||
function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
|
function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
|
||||||
const handleKeyDown = (event: {
|
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
|
||||||
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 isDisabled = isMetricsDataSource && !query.aggregateAttribute.key;
|
const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key;
|
||||||
|
|
||||||
@ -36,7 +18,7 @@ function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
|
|||||||
style={selectStyle}
|
style={selectStyle}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDownLimitFilter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,9 @@ export type OrderByFilterProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type OrderByFilterValue = {
|
export type OrderByFilterValue = {
|
||||||
disabled: boolean | undefined;
|
disabled?: boolean;
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
title: string | undefined;
|
title?: string;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
@ -53,17 +53,11 @@ export function OrderByFilter({
|
|||||||
query.groupBy,
|
query.groupBy,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isDisabledSelect = useMemo(
|
const isDisabledSelect =
|
||||||
() =>
|
|
||||||
!query.aggregateAttribute.key ||
|
!query.aggregateAttribute.key ||
|
||||||
query.aggregateOperator === MetricAggregateOperator.NOOP,
|
query.aggregateOperator === MetricAggregateOperator.NOOP;
|
||||||
[query.aggregateAttribute.key, query.aggregateOperator],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isMetricsDataSource = useMemo(
|
const isMetricsDataSource = query.dataSource === DataSource.METRICS;
|
||||||
() => query.dataSource === DataSource.METRICS,
|
|
||||||
[query.dataSource],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const FILTERS = {
|
export const ORDERBY_FILTERS = {
|
||||||
ASC: 'asc',
|
ASC: 'asc',
|
||||||
DESC: 'desc',
|
DESC: 'desc',
|
||||||
};
|
};
|
||||||
|
@ -8,18 +8,18 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
|
|||||||
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
|
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
|
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 { SIGNOZ_VALUE } from './constants';
|
||||||
import { OrderByFilterProps } from './OrderByFilter.interfaces';
|
import { OrderByFilterProps } from './OrderByFilter.interfaces';
|
||||||
import {
|
import {
|
||||||
getLabelFromValue,
|
getLabelFromValue,
|
||||||
mapLabelValuePairs,
|
mapLabelValuePairs,
|
||||||
orderByValueDelimiter,
|
orderByValueDelimiter,
|
||||||
splitOrderByFromString,
|
|
||||||
transformToOrderByStringValues,
|
transformToOrderByStringValues,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
type UseOrderByFilterResult = {
|
export type UseOrderByFilterResult = {
|
||||||
searchText: string;
|
searchText: string;
|
||||||
debouncedSearchText: string;
|
debouncedSearchText: string;
|
||||||
selectedValue: IOption[];
|
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(() => {
|
const customValue: IOption[] = useMemo(() => {
|
||||||
if (!searchText) return [];
|
if (!searchText) return [];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: `${searchText} ${FILTERS.ASC}`,
|
label: `${searchText} ${ORDERBY_FILTERS.ASC}`,
|
||||||
value: `${searchText}${orderByValueDelimiter}${FILTERS.ASC}`,
|
value: `${searchText}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `${searchText} ${FILTERS.DESC}`,
|
label: `${searchText} ${ORDERBY_FILTERS.DESC}`,
|
||||||
value: `${searchText}${orderByValueDelimiter}${FILTERS.DESC}`,
|
value: `${searchText}${orderByValueDelimiter}${ORDERBY_FILTERS.DESC}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [searchText]);
|
}, [searchText]);
|
||||||
@ -111,34 +85,9 @@ export const useOrderByFilter = ({
|
|||||||
[customValue, debouncedSearchText, selectedValue],
|
[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 handleChange = (values: IOption[]): void => {
|
||||||
const validResult = getValidResult(values);
|
const validResult = getValidOrderByResult(values);
|
||||||
const result = getUniqValues(validResult);
|
const result = getUniqueOrderByValues(validResult);
|
||||||
|
|
||||||
const orderByValues: OrderByPayload[] = result.map((item) => {
|
const orderByValues: OrderByPayload[] = result.map((item) => {
|
||||||
const match = parse(item.value, { delimiter: orderByValueDelimiter });
|
const match = parse(item.value, { delimiter: orderByValueDelimiter });
|
||||||
@ -175,12 +124,12 @@ export const useOrderByFilter = ({
|
|||||||
const aggregationOptions = useMemo(
|
const aggregationOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.ASC}`,
|
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${ORDERBY_FILTERS.ASC}`,
|
||||||
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${FILTERS.ASC}`,
|
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.DESC}`,
|
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${ORDERBY_FILTERS.DESC}`,
|
||||||
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${FILTERS.DESC}`,
|
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${ORDERBY_FILTERS.DESC}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[query],
|
[query],
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
OrderByPayload,
|
OrderByPayload,
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import { FILTERS } from './config';
|
import { ORDERBY_FILTERS } from './config';
|
||||||
import { SIGNOZ_VALUE } from './constants';
|
import { SIGNOZ_VALUE } from './constants';
|
||||||
|
|
||||||
export const orderByValueDelimiter = '|';
|
export const orderByValueDelimiter = '|';
|
||||||
@ -38,12 +38,12 @@ export function mapLabelValuePairs(
|
|||||||
const value = item.key;
|
const value = item.key;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: `${value} ${FILTERS.ASC}`,
|
label: `${value} ${ORDERBY_FILTERS.ASC}`,
|
||||||
value: `${value}${orderByValueDelimiter}${FILTERS.ASC}`,
|
value: `${value}${orderByValueDelimiter}${ORDERBY_FILTERS.ASC}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `${value} ${FILTERS.DESC}`,
|
label: `${value} ${ORDERBY_FILTERS.DESC}`,
|
||||||
value: `${value}${orderByValueDelimiter}${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 {
|
export function splitOrderByFromString(str: string): OrderByPayload | null {
|
||||||
const splittedStr = str.split(' ');
|
const splittedStr = str.split(' ');
|
||||||
const order = splittedStr.pop() || FILTERS.ASC;
|
const order = splittedStr.pop() || ORDERBY_FILTERS.ASC;
|
||||||
const columnName = splittedStr.join(' ');
|
const columnName = splittedStr.join(' ');
|
||||||
|
|
||||||
if (!columnName) return null;
|
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 {
|
import {
|
||||||
initialAutocompleteData,
|
initialAutocompleteData,
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
mapOfFilters,
|
mapOfFormulaToFilters,
|
||||||
|
mapOfQueryFilters,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
@ -9,8 +10,12 @@ import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperato
|
|||||||
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
|
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
import {
|
import {
|
||||||
|
IBuilderFormula,
|
||||||
|
IBuilderQuery,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import {
|
||||||
|
HandleChangeFormulaData,
|
||||||
HandleChangeQueryData,
|
HandleChangeQueryData,
|
||||||
UseQueryOperations,
|
UseQueryOperations,
|
||||||
} from 'types/common/operations.types';
|
} from 'types/common/operations.types';
|
||||||
@ -21,54 +26,31 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
query,
|
query,
|
||||||
index,
|
index,
|
||||||
filterConfigs,
|
filterConfigs,
|
||||||
|
formula,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
handleSetQueryData,
|
handleSetQueryData,
|
||||||
|
handleSetFormulaData,
|
||||||
removeQueryBuilderEntityByIndex,
|
removeQueryBuilderEntityByIndex,
|
||||||
panelType,
|
panelType,
|
||||||
initialDataSource,
|
initialDataSource,
|
||||||
currentQuery,
|
currentQuery,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
const [operators, setOperators] = useState<SelectOption<string, string>[]>([]);
|
const [operators, setOperators] = useState<SelectOption<string, string>[]>([]);
|
||||||
const [listOfAdditionalFilters, setListOfAdditionalFilters] = useState<
|
|
||||||
string[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const { dataSource, aggregateOperator } = query;
|
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(
|
const getNewListOfAdditionalFilters = useCallback(
|
||||||
(dataSource: DataSource): string[] => {
|
(dataSource: DataSource, isQuery: boolean): string[] => {
|
||||||
const additionalFiltersKeys: (keyof Pick<
|
const additionalFiltersKeys: (keyof Pick<
|
||||||
IBuilderQuery,
|
IBuilderQuery,
|
||||||
'orderBy' | 'limit' | 'having' | 'stepInterval'
|
'orderBy' | 'limit' | 'having' | 'stepInterval'
|
||||||
>)[] = ['having', 'limit', 'orderBy', '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) => {
|
(acc, item) => {
|
||||||
if (
|
if (
|
||||||
filterConfigs &&
|
filterConfigs &&
|
||||||
@ -91,6 +73,41 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
[filterConfigs],
|
[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(
|
const handleChangeAggregatorAttribute = useCallback(
|
||||||
(value: BaseAutocompleteData): void => {
|
(value: BaseAutocompleteData): void => {
|
||||||
const newQuery: IBuilderQuery = {
|
const newQuery: IBuilderQuery = {
|
||||||
@ -148,6 +165,18 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
[query, index, handleSetQueryData],
|
[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 isMetricsDataSource = query.dataSource === DataSource.METRICS;
|
||||||
|
|
||||||
const isTracePanelType = panelType === PANEL_TYPES.TRACE;
|
const isTracePanelType = panelType === PANEL_TYPES.TRACE;
|
||||||
@ -166,11 +195,17 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
}, [dataSource, initialDataSource, panelType, operators]);
|
}, [dataSource, initialDataSource, panelType, operators]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const additionalFilters = getNewListOfAdditionalFilters(dataSource);
|
const additionalFilters = getNewListOfAdditionalFilters(dataSource, true);
|
||||||
|
|
||||||
setListOfAdditionalFilters(additionalFilters);
|
setListOfAdditionalFilters(additionalFilters);
|
||||||
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
|
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const additionalFilters = getNewListOfAdditionalFilters(dataSource, false);
|
||||||
|
|
||||||
|
setListOfAdditionalFormulaFilters(additionalFilters);
|
||||||
|
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isTracePanelType,
|
isTracePanelType,
|
||||||
isMetricsDataSource,
|
isMetricsDataSource,
|
||||||
@ -181,5 +216,7 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
handleChangeDataSource,
|
handleChangeDataSource,
|
||||||
handleDeleteQuery,
|
handleDeleteQuery,
|
||||||
handleChangeQueryData,
|
handleChangeQueryData,
|
||||||
|
listOfAdditionalFormulaFilters,
|
||||||
|
handleChangeFormulaData,
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -19,12 +19,12 @@ export const getOperatorsBySourceAndPanelType = ({
|
|||||||
let operatorsByDataSource = mapOfOperators[dataSource];
|
let operatorsByDataSource = mapOfOperators[dataSource];
|
||||||
|
|
||||||
if (panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE) {
|
if (panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE) {
|
||||||
operatorsByDataSource = operatorsByDataSource.filter(
|
operatorsByDataSource = operatorsByDataSource?.filter(
|
||||||
(operator) => operator.value === StringOperators.NOOP,
|
(operator) => operator.value === StringOperators.NOOP,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (panelType === PANEL_TYPES.TABLE && dataSource === DataSource.METRICS) {
|
if (panelType === PANEL_TYPES.TABLE && dataSource === DataSource.METRICS) {
|
||||||
operatorsByDataSource = operatorsByDataSource.filter(
|
operatorsByDataSource = operatorsByDataSource?.filter(
|
||||||
(operator) =>
|
(operator) =>
|
||||||
operator.value !== MetricAggregateOperator.NOOP &&
|
operator.value !== MetricAggregateOperator.NOOP &&
|
||||||
operator.value !== MetricAggregateOperator.RATE,
|
operator.value !== MetricAggregateOperator.RATE,
|
||||||
@ -35,7 +35,7 @@ export const getOperatorsBySourceAndPanelType = ({
|
|||||||
panelType !== PANEL_TYPES.LIST &&
|
panelType !== PANEL_TYPES.LIST &&
|
||||||
panelType !== PANEL_TYPES.TRACE
|
panelType !== PANEL_TYPES.TRACE
|
||||||
) {
|
) {
|
||||||
operatorsByDataSource = operatorsByDataSource.filter(
|
operatorsByDataSource = operatorsByDataSource?.filter(
|
||||||
(operator) => operator.value !== StringOperators.NOOP,
|
(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 { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import {
|
import {
|
||||||
IBuilderQuery,
|
IBuilderQuery,
|
||||||
@ -51,7 +52,10 @@ export const getPaginationQueryData: SetupPaginationQueryData = ({
|
|||||||
dataType: DataTypes.String,
|
dataType: DataTypes.String,
|
||||||
isColumn: true,
|
isColumn: true,
|
||||||
},
|
},
|
||||||
op: orderByTimestamp.order === FILTERS.ASC ? '>' : '<',
|
op:
|
||||||
|
orderByTimestamp.order === ORDERBY_FILTERS.ASC
|
||||||
|
? OPERATORS['>']
|
||||||
|
: OPERATORS['<'],
|
||||||
value: listItemId,
|
value: listItemId,
|
||||||
},
|
},
|
||||||
...updatedFilters.items,
|
...updatedFilters.items,
|
||||||
|
@ -13,8 +13,11 @@ export interface IBuilderFormula {
|
|||||||
expression: string;
|
expression: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
queryName: string;
|
queryName: string;
|
||||||
dataSource?: DataSource;
|
|
||||||
legend: string;
|
legend: string;
|
||||||
|
limit?: number | null;
|
||||||
|
having?: Having[];
|
||||||
|
stepInterval?: number;
|
||||||
|
orderBy?: OrderByPayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TagFilterItem {
|
export interface TagFilterItem {
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces';
|
import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces';
|
||||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
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 { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { SelectOption } from './select';
|
import { SelectOption } from './select';
|
||||||
|
|
||||||
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
|
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
|
||||||
Pick<QueryBuilderProps, 'filterConfigs'>;
|
Pick<QueryBuilderProps, 'filterConfigs'> & {
|
||||||
|
formula?: IBuilderFormula;
|
||||||
|
};
|
||||||
|
|
||||||
export type HandleChangeQueryData = <
|
export type HandleChangeQueryData = <
|
||||||
Key extends keyof IBuilderQuery,
|
Key extends keyof IBuilderQuery,
|
||||||
@ -17,6 +22,14 @@ export type HandleChangeQueryData = <
|
|||||||
value: Value,
|
value: Value,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
export type HandleChangeFormulaData = <
|
||||||
|
Key extends keyof IBuilderFormula,
|
||||||
|
Value extends IBuilderFormula[Key]
|
||||||
|
>(
|
||||||
|
key: Key,
|
||||||
|
value: Value,
|
||||||
|
) => void;
|
||||||
|
|
||||||
export type UseQueryOperations = (
|
export type UseQueryOperations = (
|
||||||
params: UseQueryOperationsParams,
|
params: UseQueryOperationsParams,
|
||||||
) => {
|
) => {
|
||||||
@ -29,4 +42,6 @@ export type UseQueryOperations = (
|
|||||||
handleChangeDataSource: (newSource: DataSource) => void;
|
handleChangeDataSource: (newSource: DataSource) => void;
|
||||||
handleDeleteQuery: () => void;
|
handleDeleteQuery: () => void;
|
||||||
handleChangeQueryData: HandleChangeQueryData;
|
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 {
|
for _, result := range results {
|
||||||
builderQueries := queryRangeParams.CompositeQuery.BuilderQueries
|
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
|
limit := builderQueries[result.QueryName].Limit
|
||||||
|
|
||||||
orderByList := builderQueries[result.QueryName].OrderBy
|
orderByList := builderQueries[result.QueryName].OrderBy
|
||||||
|
@ -87,7 +87,12 @@ func unique(slice []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// expressionToQuery constructs the query for the expression
|
// 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
|
var formulaQuery string
|
||||||
variables := unique(expression.Vars())
|
variables := unique(expression.Vars())
|
||||||
|
|
||||||
@ -134,6 +139,14 @@ func expressionToQuery(qp *v3.QueryRangeParamsV3, varToQuery map[string]string,
|
|||||||
prevVar = variable
|
prevVar = variable
|
||||||
}
|
}
|
||||||
formulaQuery = fmt.Sprintf("SELECT %s, %s as value FROM ", joinUsing, formula.ExpressionString()) + formulaSubQuery
|
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
|
return formulaQuery, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +249,7 @@ func (qb *QueryBuilder) PrepareQueries(params *v3.QueryRangeParamsV3, args ...in
|
|||||||
if query.Expression != query.QueryName {
|
if query.Expression != query.QueryName {
|
||||||
expression, _ := govaluate.NewEvaluableExpressionWithFunctions(query.Expression, EvalFuncs)
|
expression, _ := govaluate.NewEvaluableExpressionWithFunctions(query.Expression, EvalFuncs)
|
||||||
|
|
||||||
queryString, err := expressionToQuery(params, queries, expression)
|
queryString, err := expressionToQuery(params, queries, expression, query.QueryName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user