mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 00:38:59 +08:00
feat(builder): add having filter (#2567)
* feat(builder): add having filter * feat(builder): add having filter * feat(builder): add having filter * feat: return initial query builder * fix: having filter operators and values
This commit is contained in:
parent
63570c847a
commit
dd25ad95c7
@ -1,9 +1,11 @@
|
|||||||
// ** TODO: use it for creating formula names
|
|
||||||
// import { createNewFormulaName } from 'lib/newQueryBuilder/createNewFormulaName';
|
|
||||||
// ** Helpers
|
// ** Helpers
|
||||||
import { createNewQueryName } from 'lib/newQueryBuilder/createNewQueryName';
|
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||||
import { LocalDataType } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { LocalDataType } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
import {
|
||||||
|
Having,
|
||||||
|
IBuilderFormula,
|
||||||
|
IBuilderQueryForm,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import {
|
import {
|
||||||
BoolOperators,
|
BoolOperators,
|
||||||
DataSource,
|
DataSource,
|
||||||
@ -14,6 +16,16 @@ import {
|
|||||||
TracesAggregatorOperator,
|
TracesAggregatorOperator,
|
||||||
} from 'types/common/queryBuilder';
|
} from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export const MAX_FORMULAS = 20;
|
||||||
|
export const MAX_QUERIES = 26;
|
||||||
|
|
||||||
|
export const formulasNames: string[] = Array.from(
|
||||||
|
Array(MAX_FORMULAS),
|
||||||
|
(_, i) => `F${i + 1}`,
|
||||||
|
);
|
||||||
|
const alpha: number[] = Array.from(Array(MAX_QUERIES), (_, i) => i + 65);
|
||||||
|
export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
|
||||||
|
|
||||||
export enum QueryBuilderKeys {
|
export enum QueryBuilderKeys {
|
||||||
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
||||||
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
|
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
|
||||||
@ -27,9 +39,15 @@ export const mapOfOperators: Record<DataSource, string[]> = {
|
|||||||
|
|
||||||
export const mapOfFilters: Record<DataSource, string[]> = {
|
export const mapOfFilters: Record<DataSource, string[]> = {
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
metrics: ['Having', 'Aggregation interval'],
|
metrics: ['Aggregation interval', 'Having'],
|
||||||
logs: ['Limit', 'Having', 'Order by', 'Aggregation interval'],
|
logs: ['Order by', 'Limit', 'Having', 'Aggregation interval'],
|
||||||
traces: ['Limit', 'Having', 'Order by', 'Aggregation interval'],
|
traces: ['Order by', 'Limit', 'Having', 'Aggregation interval'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialHavingValues: Having = {
|
||||||
|
columnName: '',
|
||||||
|
op: '',
|
||||||
|
value: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialAggregateAttribute: IBuilderQueryForm['aggregateAttribute'] = {
|
export const initialAggregateAttribute: IBuilderQueryForm['aggregateAttribute'] = {
|
||||||
@ -41,7 +59,7 @@ export const initialAggregateAttribute: IBuilderQueryForm['aggregateAttribute']
|
|||||||
|
|
||||||
export const initialQueryBuilderFormValues: IBuilderQueryForm = {
|
export const initialQueryBuilderFormValues: IBuilderQueryForm = {
|
||||||
dataSource: DataSource.METRICS,
|
dataSource: DataSource.METRICS,
|
||||||
queryName: createNewQueryName([]),
|
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
||||||
aggregateOperator: Object.values(MetricAggregateOperator)[0],
|
aggregateOperator: Object.values(MetricAggregateOperator)[0],
|
||||||
aggregateAttribute: initialAggregateAttribute,
|
aggregateAttribute: initialAggregateAttribute,
|
||||||
tagFilters: { items: [], op: 'AND' },
|
tagFilters: { items: [], op: 'AND' },
|
||||||
@ -56,6 +74,16 @@ export const initialQueryBuilderFormValues: IBuilderQueryForm = {
|
|||||||
reduceTo: '',
|
reduceTo: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initialFormulaBuilderFormValues: IBuilderFormula = {
|
||||||
|
label: createNewBuilderItemName({
|
||||||
|
existNames: [],
|
||||||
|
sourceNames: formulasNames,
|
||||||
|
}),
|
||||||
|
expression: '',
|
||||||
|
disabled: false,
|
||||||
|
legend: '',
|
||||||
|
};
|
||||||
|
|
||||||
export const operatorsByTypes: Record<LocalDataType, string[]> = {
|
export const operatorsByTypes: Record<LocalDataType, string[]> = {
|
||||||
string: Object.values(StringOperators),
|
string: Object.values(StringOperators),
|
||||||
number: Object.values(NumberOperators),
|
number: Object.values(NumberOperators),
|
||||||
@ -136,3 +164,14 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
|||||||
OPERATORS.NOT_CONTAINS,
|
OPERATORS.NOT_CONTAINS,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const HAVING_OPERATORS: string[] = [
|
||||||
|
OPERATORS.EQUALS,
|
||||||
|
OPERATORS.NOT_EQUALS,
|
||||||
|
OPERATORS.IN,
|
||||||
|
OPERATORS.NIN,
|
||||||
|
OPERATORS.GTE,
|
||||||
|
OPERATORS.GT,
|
||||||
|
OPERATORS.LTE,
|
||||||
|
OPERATORS.LT,
|
||||||
|
];
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { Button, Col, Row } from 'antd';
|
import { Button, Col, Row } from 'antd';
|
||||||
|
import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder';
|
||||||
// ** Hooks
|
// ** Hooks
|
||||||
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
||||||
import { MAX_FORMULAS } from 'lib/newQueryBuilder/createNewFormulaName';
|
|
||||||
// ** Constants
|
// ** Constants
|
||||||
import { MAX_QUERIES } from 'lib/newQueryBuilder/createNewQueryName';
|
|
||||||
import React, { memo, useEffect, useMemo } from 'react';
|
import React, { memo, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
// ** Components
|
// ** Components
|
||||||
import { Query } from './components';
|
import { Formula, Query } from './components';
|
||||||
// ** Types
|
// ** Types
|
||||||
import { QueryBuilderProps } from './QueryBuilder.interfaces';
|
import { QueryBuilderProps } from './QueryBuilder.interfaces';
|
||||||
// ** Styles
|
// ** Styles
|
||||||
@ -21,6 +20,7 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
queryBuilderData,
|
queryBuilderData,
|
||||||
setupInitialDataSource,
|
setupInitialDataSource,
|
||||||
addNewQuery,
|
addNewQuery,
|
||||||
|
addNewFormula,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -39,7 +39,7 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isDisabledFormulaButton = useMemo(
|
const isDisabledFormulaButton = useMemo(
|
||||||
() => queryBuilderData.queryData.length >= MAX_FORMULAS,
|
() => queryBuilderData.queryFormulas.length >= MAX_FORMULAS,
|
||||||
[queryBuilderData],
|
[queryBuilderData],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -58,6 +58,11 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
|
{queryBuilderData.queryFormulas.map((formula, index) => (
|
||||||
|
<Col key={formula.label} span={24}>
|
||||||
|
<Formula formula={formula} index={index} />
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
@ -75,6 +80,7 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
<Col>
|
<Col>
|
||||||
<Button
|
<Button
|
||||||
disabled={isDisabledFormulaButton}
|
disabled={isDisabledFormulaButton}
|
||||||
|
onClick={addNewFormula}
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
>
|
>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons';
|
import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons';
|
||||||
import { Typography } from 'antd';
|
import { Col, Typography } from 'antd';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
const IconCss = css`
|
const IconCss = css`
|
||||||
@ -15,14 +15,8 @@ export const StyledIconClose = styled(MinusSquareOutlined)`
|
|||||||
${IconCss}
|
${IconCss}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledWrapper = styled.div`
|
export const StyledInner = styled(Col)`
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledInner = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0.875rem;
|
margin-bottom: 0.875rem;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Col, Row } from 'antd';
|
||||||
import React, { Fragment, memo, ReactNode, useState } from 'react';
|
import React, { Fragment, memo, ReactNode, useState } from 'react';
|
||||||
|
|
||||||
// ** Types
|
// ** Types
|
||||||
@ -39,12 +40,14 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Row>
|
||||||
<StyledInner onClick={handleToggleOpenFilters}>
|
<Col span={24}>
|
||||||
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
|
<StyledInner onClick={handleToggleOpenFilters}>
|
||||||
{!isOpenedFilters && <span>Add conditions for {filtersTexts}</span>}
|
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
|
||||||
</StyledInner>
|
{!isOpenedFilters && <span>Add conditions for {filtersTexts}</span>}
|
||||||
{isOpenedFilters && children}
|
</StyledInner>
|
||||||
</div>
|
</Col>
|
||||||
|
{isOpenedFilters && <Col span={24}>{children}</Col>}
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2,8 +2,8 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
export const StyledLabel = styled.div`
|
export const StyledLabel = styled.div`
|
||||||
padding: 0 0.6875rem;
|
padding: 0 0.6875rem;
|
||||||
width: fit-content;
|
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
|
width: 100%;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
// TODO: temporary type
|
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
export type FormulaProps = { test: string };
|
|
||||||
|
export type FormulaProps = { formula: IBuilderFormula; index: number };
|
||||||
|
@ -1,5 +1,73 @@
|
|||||||
import React from 'react';
|
import { Col, Input } from 'antd';
|
||||||
|
// ** Components
|
||||||
|
import { ListItemWrapper, ListMarker } from 'container/QueryBuilder/components';
|
||||||
|
// ** Hooks
|
||||||
|
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
||||||
|
import React, { ChangeEvent, useCallback } from 'react';
|
||||||
|
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
export function Formula(): JSX.Element {
|
// ** Types
|
||||||
return <div>null</div>;
|
import { FormulaProps } from './Formula.interfaces';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
export function Formula({ index, formula }: FormulaProps): JSX.Element {
|
||||||
|
const { removeEntityByIndex, handleSetFormulaData } = useQueryBuilder();
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
removeEntityByIndex('queryFormulas', index);
|
||||||
|
}, [index, removeEntityByIndex]);
|
||||||
|
|
||||||
|
const handleToggleDisableFormula = useCallback((): void => {
|
||||||
|
const newFormula: IBuilderFormula = {
|
||||||
|
...formula,
|
||||||
|
disabled: !formula.disabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetFormulaData(index, newFormula);
|
||||||
|
}, [index, formula, handleSetFormulaData]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
const newFormula: IBuilderFormula = {
|
||||||
|
...formula,
|
||||||
|
[name]: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSetFormulaData(index, newFormula);
|
||||||
|
},
|
||||||
|
[index, formula, handleSetFormulaData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemWrapper onDelete={handleDelete}>
|
||||||
|
<Col span={24}>
|
||||||
|
<ListMarker
|
||||||
|
isDisabled={formula.disabled}
|
||||||
|
onDisable={handleToggleDisableFormula}
|
||||||
|
labelName={formula.label}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<TextArea
|
||||||
|
name="expression"
|
||||||
|
onChange={handleChange}
|
||||||
|
size="middle"
|
||||||
|
value={formula.expression}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={24}>
|
||||||
|
<Input
|
||||||
|
name="legend"
|
||||||
|
onChange={handleChange}
|
||||||
|
size="middle"
|
||||||
|
value={formula.legend}
|
||||||
|
addonBefore="Legend Format"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</ListItemWrapper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
export type ListItemWrapperProps = PropsWithChildren & { onDelete: () => void };
|
@ -4,7 +4,7 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
export const StyledDeleteEntity = styled(CloseCircleOutlined)`
|
export const StyledDeleteEntity = styled(CloseCircleOutlined)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.9375rem;
|
top: 0.5rem;
|
||||||
right: 0.9375rem;
|
right: 0.9375rem;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { ListItemWrapperProps } from './ListItemWrapper.interfaces';
|
||||||
|
// ** Styles
|
||||||
|
import { StyledDeleteEntity, StyledRow } from './ListItemWrapper.styled';
|
||||||
|
|
||||||
|
export function ListItemWrapper({
|
||||||
|
children,
|
||||||
|
onDelete,
|
||||||
|
}: ListItemWrapperProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<StyledRow gutter={[0, 15]}>
|
||||||
|
<StyledDeleteEntity onClick={onDelete} />
|
||||||
|
{children}
|
||||||
|
</StyledRow>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { ListItemWrapper } from './ListItemWrapper';
|
@ -5,7 +5,7 @@ export type ListMarkerProps = {
|
|||||||
labelName: string;
|
labelName: string;
|
||||||
index: number;
|
index: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
isAvailableToDisable: boolean;
|
isAvailableToDisable?: boolean;
|
||||||
toggleDisabled: (index: number) => void;
|
onDisable: (index: number) => void;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
};
|
};
|
||||||
|
@ -11,16 +11,16 @@ export const ListMarker = memo(function ListMarker({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
labelName,
|
labelName,
|
||||||
index,
|
index,
|
||||||
isAvailableToDisable,
|
isAvailableToDisable = true,
|
||||||
className,
|
className,
|
||||||
toggleDisabled,
|
onDisable,
|
||||||
style,
|
style,
|
||||||
}: ListMarkerProps): JSX.Element {
|
}: ListMarkerProps): JSX.Element {
|
||||||
const buttonProps: Partial<ButtonProps> = isAvailableToDisable
|
const buttonProps: Partial<ButtonProps> = isAvailableToDisable
|
||||||
? {
|
? {
|
||||||
type: isDisabled ? 'default' : 'primary',
|
type: isDisabled ? 'default' : 'primary',
|
||||||
icon: isDisabled ? <EyeInvisibleFilled /> : <EyeFilled />,
|
icon: isDisabled ? <EyeInvisibleFilled /> : <EyeFilled />,
|
||||||
onClick: (): void => toggleDisabled(index),
|
onClick: (): void => onDisable(index),
|
||||||
}
|
}
|
||||||
: { type: 'primary' };
|
: { type: 'primary' };
|
||||||
|
|
||||||
|
@ -11,11 +11,13 @@ import {
|
|||||||
AdditionalFiltersToggler,
|
AdditionalFiltersToggler,
|
||||||
DataSourceDropdown,
|
DataSourceDropdown,
|
||||||
FilterLabel,
|
FilterLabel,
|
||||||
|
ListItemWrapper,
|
||||||
ListMarker,
|
ListMarker,
|
||||||
} from 'container/QueryBuilder/components';
|
} from 'container/QueryBuilder/components';
|
||||||
import {
|
import {
|
||||||
AggregatorFilter,
|
AggregatorFilter,
|
||||||
GroupByFilter,
|
GroupByFilter,
|
||||||
|
HavingFilter,
|
||||||
OperatorsSelect,
|
OperatorsSelect,
|
||||||
ReduceToFilter,
|
ReduceToFilter,
|
||||||
} from 'container/QueryBuilder/filters';
|
} from 'container/QueryBuilder/filters';
|
||||||
@ -29,16 +31,15 @@ import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
|
|||||||
import React, { memo, useCallback, useMemo } from 'react';
|
import React, { memo, useCallback, useMemo } from 'react';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import {
|
import {
|
||||||
|
Having,
|
||||||
IBuilderQueryForm,
|
IBuilderQueryForm,
|
||||||
TagFilter,
|
TagFilter,
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
import { transformToUpperCase } from 'utils/transformToUpperCase';
|
||||||
|
|
||||||
// ** Types
|
// ** Types
|
||||||
import { QueryProps } from './Query.interfaces';
|
import { QueryProps } from './Query.interfaces';
|
||||||
// ** Styles
|
|
||||||
import { StyledDeleteEntity, StyledFilterRow, StyledRow } from './Query.styled';
|
|
||||||
|
|
||||||
export const Query = memo(function Query({
|
export const Query = memo(function Query({
|
||||||
index,
|
index,
|
||||||
@ -116,6 +117,7 @@ export const Query = memo(function Query({
|
|||||||
const newQuery: IBuilderQueryForm = {
|
const newQuery: IBuilderQueryForm = {
|
||||||
...query,
|
...query,
|
||||||
aggregateAttribute: value,
|
aggregateAttribute: value,
|
||||||
|
having: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSetQueryData(index, newQuery);
|
handleSetQueryData(index, newQuery);
|
||||||
@ -192,6 +194,15 @@ export const Query = memo(function Query({
|
|||||||
[index, query, handleSetQueryData],
|
[index, query, handleSetQueryData],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleChangeHavingFilter = useCallback(
|
||||||
|
(having: Having[]) => {
|
||||||
|
const newQuery: IBuilderQueryForm = { ...query, having };
|
||||||
|
|
||||||
|
handleSetQueryData(index, newQuery);
|
||||||
|
},
|
||||||
|
[index, query, handleSetQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDeleteQuery = useCallback(() => {
|
const handleDeleteQuery = useCallback(() => {
|
||||||
removeEntityByIndex('queryData', index);
|
removeEntityByIndex('queryData', index);
|
||||||
}, [removeEntityByIndex, index]);
|
}, [removeEntityByIndex, index]);
|
||||||
@ -246,14 +257,13 @@ export const Query = memo(function Query({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRow gutter={[0, 15]}>
|
<ListItemWrapper onDelete={handleDeleteQuery}>
|
||||||
<StyledDeleteEntity onClick={handleDeleteQuery} />
|
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Row align="middle" justify="space-between">
|
<Row align="middle">
|
||||||
<Col>
|
<Col>
|
||||||
<ListMarker
|
<ListMarker
|
||||||
isDisabled={query.disabled}
|
isDisabled={query.disabled}
|
||||||
toggleDisabled={handleToggleDisableQuery}
|
onDisable={handleToggleDisableQuery}
|
||||||
labelName={query.queryName}
|
labelName={query.queryName}
|
||||||
index={index}
|
index={index}
|
||||||
isAvailableToDisable={isAvailableToDisable}
|
isAvailableToDisable={isAvailableToDisable}
|
||||||
@ -267,10 +277,18 @@ export const Query = memo(function Query({
|
|||||||
) : (
|
) : (
|
||||||
<FilterLabel label={transformToUpperCase(query.dataSource)} />
|
<FilterLabel label={transformToUpperCase(query.dataSource)} />
|
||||||
)}
|
)}
|
||||||
{isMatricsDataSource && <FilterLabel label="WHERE" />}
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={isMatricsDataSource ? 17 : 20}>
|
<Col flex="1">
|
||||||
<QueryBuilderSearch query={query} onChange={handleChangeTagFilters} />
|
<Row gutter={[11, 5]}>
|
||||||
|
{isMatricsDataSource && (
|
||||||
|
<Col>
|
||||||
|
<FilterLabel label="WHERE" />
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
<Col flex="1">
|
||||||
|
<QueryBuilderSearch query={query} onChange={handleChangeTagFilters} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
@ -291,6 +309,7 @@ export const Query = memo(function Query({
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={11} offset={2}>
|
<Col span={11} offset={2}>
|
||||||
<Row gutter={[11, 5]}>
|
<Row gutter={[11, 5]}>
|
||||||
<Col flex="5.93rem">
|
<Col flex="5.93rem">
|
||||||
@ -307,31 +326,56 @@ export const Query = memo(function Query({
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
|
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
|
||||||
{!isMatricsDataSource && (
|
<Row gutter={[0, 11]} justify="space-between">
|
||||||
<StyledFilterRow gutter={[11, 5]} justify="space-around">
|
{!isMatricsDataSource && (
|
||||||
<Col span={2}>
|
<Col span={11}>
|
||||||
<FilterLabel label="Order by" />
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="5.93rem">
|
||||||
|
<FilterLabel label="Limit" />
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 12.5rem">
|
||||||
|
<LimitFilter query={query} onChange={handleChangeLimit} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={10}>
|
)}
|
||||||
<OrderByFilter query={query} onChange={handleChangeOrderByKeys} />
|
{query.aggregateOperator !== StringOperators.NOOP && (
|
||||||
|
<Col span={11}>
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="5.93rem">
|
||||||
|
<FilterLabel label="HAVING" />
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 12.5rem">
|
||||||
|
<HavingFilter onChange={handleChangeHavingFilter} query={query} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={1.5}>
|
)}
|
||||||
<FilterLabel label="Limit" />
|
{!isMatricsDataSource && (
|
||||||
|
<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} onChange={handleChangeOrderByKeys} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={10}>
|
)}
|
||||||
<LimitFilter query={query} onChange={handleChangeLimit} />
|
|
||||||
</Col>
|
<Col span={11}>
|
||||||
</StyledFilterRow>
|
<Row gutter={[11, 5]}>
|
||||||
)}
|
<Col flex="5.93rem">
|
||||||
<Row gutter={[11, 5]}>
|
<FilterLabel label="Aggregate Every" />
|
||||||
<Col span={3}>
|
</Col>
|
||||||
<FilterLabel label="Aggregate Every" />
|
<Col flex="1 1 6rem">
|
||||||
</Col>
|
<AggregateEveryFilter
|
||||||
<Col span={8}>
|
query={query}
|
||||||
<AggregateEveryFilter
|
onChange={handleChangeAggregateEvery}
|
||||||
query={query}
|
/>
|
||||||
onChange={handleChangeAggregateEvery}
|
</Col>
|
||||||
/>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</AdditionalFiltersToggler>
|
</AdditionalFiltersToggler>
|
||||||
@ -344,6 +388,6 @@ export const Query = memo(function Query({
|
|||||||
addonBefore="Legend Format"
|
addonBefore="Legend Format"
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</StyledRow>
|
</ListItemWrapper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2,5 +2,6 @@ export { AdditionalFiltersToggler } from './AdditionalFiltersToggler';
|
|||||||
export { DataSourceDropdown } from './DataSourceDropdown';
|
export { DataSourceDropdown } from './DataSourceDropdown';
|
||||||
export { FilterLabel } from './FilterLabel';
|
export { FilterLabel } from './FilterLabel';
|
||||||
export { Formula } from './Formula';
|
export { Formula } from './Formula';
|
||||||
|
export { ListItemWrapper } from './ListItemWrapper';
|
||||||
export { ListMarker } from './ListMarker';
|
export { ListMarker } from './ListMarker';
|
||||||
export { Query } from './Query';
|
export { Query } from './Query';
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import {
|
||||||
|
Having,
|
||||||
|
IBuilderQueryForm,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export type HavingFilterProps = {
|
||||||
|
query: IBuilderQueryForm;
|
||||||
|
onChange: (having: Having[]) => void;
|
||||||
|
};
|
@ -0,0 +1,196 @@
|
|||||||
|
import { Select } from 'antd';
|
||||||
|
// ** Constants
|
||||||
|
import { HAVING_OPERATORS, initialHavingValues } from 'constants/queryBuilder';
|
||||||
|
// ** Hooks
|
||||||
|
import { useTagValidation } from 'hooks/queryBuilder/useTagValidation';
|
||||||
|
import {
|
||||||
|
transformFromStringToHaving,
|
||||||
|
transformHavingToStringValue,
|
||||||
|
} from 'lib/query/transformQueryBuilderData';
|
||||||
|
// ** Helpers
|
||||||
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Having } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { SelectOption } from 'types/common/select';
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { HavingFilterProps } from './HavingFilter.interfaces';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
export function HavingFilter({
|
||||||
|
query,
|
||||||
|
onChange,
|
||||||
|
}: HavingFilterProps): JSX.Element {
|
||||||
|
const { having } = query;
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
const [options, setOptions] = useState<SelectOption<string, string>[]>([]);
|
||||||
|
const [localValues, setLocalValues] = useState<string[]>([]);
|
||||||
|
const [currentFormValue, setCurrentFormValue] = useState<Having>(
|
||||||
|
initialHavingValues,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isMulti } = useTagValidation(
|
||||||
|
searchText,
|
||||||
|
currentFormValue.op,
|
||||||
|
currentFormValue.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const aggregatorAttribute = useMemo(
|
||||||
|
() =>
|
||||||
|
transformStringWithPrefix({
|
||||||
|
str: query.aggregateAttribute.key,
|
||||||
|
prefix: query.aggregateAttribute.type || '',
|
||||||
|
condition: !query.aggregateAttribute.isColumn,
|
||||||
|
}),
|
||||||
|
[query],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnName = useMemo(
|
||||||
|
() => `${query.aggregateOperator.toUpperCase()}(${aggregatorAttribute})`,
|
||||||
|
[query, aggregatorAttribute],
|
||||||
|
);
|
||||||
|
|
||||||
|
const aggregatorOptions: SelectOption<string, string>[] = useMemo(
|
||||||
|
() => [{ label: columnName, value: columnName }],
|
||||||
|
[columnName],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getHavingObject = useCallback((currentSearch: string): Having => {
|
||||||
|
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(' ');
|
||||||
|
let newOptions: SelectOption<string, string>[] = [];
|
||||||
|
|
||||||
|
const isAggregatorExist = columnName
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search.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);
|
||||||
|
},
|
||||||
|
[columnName, aggregatorOptions],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isValidHavingValue = (search: string): boolean => {
|
||||||
|
const values = getHavingObject(search).value.join(' ');
|
||||||
|
if (values) {
|
||||||
|
const numRegexp = /^[^a-zA-Z]*$/;
|
||||||
|
|
||||||
|
return numRegexp.test(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetChanges = (): void => {
|
||||||
|
handleSearch('');
|
||||||
|
setCurrentFormValue(initialHavingValues);
|
||||||
|
setOptions(aggregatorOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (values: string[]): void => {
|
||||||
|
const having: Having[] = values.map(transformFromStringToHaving);
|
||||||
|
|
||||||
|
const isSelectable: boolean =
|
||||||
|
currentFormValue.value.length > 0 &&
|
||||||
|
currentFormValue.value.every((value) => !!value);
|
||||||
|
|
||||||
|
if (isSelectable) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
handleSearch(isClearSearch ? '' : currentValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseSearchText = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
const { columnName, op, value } = getHavingObject(text);
|
||||||
|
setCurrentFormValue({ columnName, op, value });
|
||||||
|
|
||||||
|
generateOptions(text);
|
||||||
|
},
|
||||||
|
[generateOptions, getHavingObject],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeselect = (value: string): void => {
|
||||||
|
const result = localValues.filter((item) => item !== value);
|
||||||
|
setLocalValues(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
parseSearchText(searchText);
|
||||||
|
}, [searchText, parseSearchText]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalValues(transformHavingToStringValue(having));
|
||||||
|
}, [having]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
onSearch={handleSearch}
|
||||||
|
searchValue={searchText}
|
||||||
|
value={localValues}
|
||||||
|
data-testid="havingSelect"
|
||||||
|
disabled={!query.aggregateAttribute.key}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
notFoundContent={currentFormValue.value.length === 0 ? undefined : null}
|
||||||
|
placeholder="Count(operation) > 5"
|
||||||
|
onDeselect={handleDeselect}
|
||||||
|
onBlur={resetChanges}
|
||||||
|
onChange={handleChange}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<Option key={opt.value} value={opt.value} title="havingOption">
|
||||||
|
{opt.label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
// Constants
|
||||||
|
import {
|
||||||
|
HAVING_OPERATORS,
|
||||||
|
initialQueryBuilderFormValues,
|
||||||
|
} from 'constants/queryBuilder';
|
||||||
|
import { transformFromStringToHaving } from 'lib/query/transformQueryBuilderData';
|
||||||
|
import React from 'react';
|
||||||
|
// ** Types
|
||||||
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
// ** Components
|
||||||
|
import { HavingFilter } from '../HavingFilter';
|
||||||
|
|
||||||
|
const valueWithAttributeAndOperator: IBuilderQueryForm = {
|
||||||
|
...initialQueryBuilderFormValues,
|
||||||
|
dataSource: DataSource.LOGS,
|
||||||
|
aggregateOperator: 'SUM',
|
||||||
|
aggregateAttribute: {
|
||||||
|
isColumn: false,
|
||||||
|
key: 'bytes',
|
||||||
|
type: 'tag',
|
||||||
|
dataType: 'float64',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Having filter behaviour', () => {
|
||||||
|
test('Having filter render is rendered', () => {
|
||||||
|
const mockFn = jest.fn();
|
||||||
|
const { unmount } = render(
|
||||||
|
<HavingFilter query={initialQueryBuilderFormValues} onChange={mockFn} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectId = 'havingSelect';
|
||||||
|
|
||||||
|
const select = screen.getByTestId(selectId);
|
||||||
|
|
||||||
|
expect(select).toBeInTheDocument();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Having render is disabled initially', () => {
|
||||||
|
const mockFn = jest.fn();
|
||||||
|
const { unmount } = render(
|
||||||
|
<HavingFilter query={initialQueryBuilderFormValues} onChange={mockFn} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
expect(input).toBeDisabled();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Is having filter is enable', () => {
|
||||||
|
const mockFn = jest.fn();
|
||||||
|
const { unmount } = render(
|
||||||
|
<HavingFilter query={valueWithAttributeAndOperator} onChange={mockFn} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
expect(input).toBeEnabled();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Autocomplete in the having filter', async () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const constructedAttribute = 'SUM(tag_bytes)';
|
||||||
|
const optionTestTitle = 'havingOption';
|
||||||
|
|
||||||
|
const { unmount } = render(
|
||||||
|
<HavingFilter query={valueWithAttributeAndOperator} onChange={onChange} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
// get input
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
// click on the select
|
||||||
|
await user.click(input);
|
||||||
|
|
||||||
|
// show predefined options for operator with attribute SUM(tag_bytes)
|
||||||
|
const option = screen.getByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
expect(option).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(option);
|
||||||
|
|
||||||
|
// autocomplete input
|
||||||
|
expect(input).toHaveValue(constructedAttribute);
|
||||||
|
|
||||||
|
// clear value from input and write from keyboard
|
||||||
|
await user.clear(input);
|
||||||
|
|
||||||
|
await user.type(input, 'bytes');
|
||||||
|
|
||||||
|
// show again predefined options for operator with attribute SUM(tag_bytes)
|
||||||
|
const sameAttributeOption = screen.getByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
expect(sameAttributeOption).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(sameAttributeOption);
|
||||||
|
|
||||||
|
expect(input).toHaveValue(constructedAttribute);
|
||||||
|
|
||||||
|
// show operators after SUM(tag_bytes)
|
||||||
|
const operatorsOptions = screen.getAllByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
expect(operatorsOptions.length).toEqual(HAVING_OPERATORS.length);
|
||||||
|
|
||||||
|
// show operators after SUM(tag_bytes) when type from keyboard
|
||||||
|
await user.clear(input);
|
||||||
|
|
||||||
|
await user.type(input, `${constructedAttribute} !=`);
|
||||||
|
|
||||||
|
// get filtered operators
|
||||||
|
const filteredOperators = screen.getAllByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
expect(filteredOperators.length).toEqual(1);
|
||||||
|
|
||||||
|
// clear and show again all operators
|
||||||
|
await user.clear(input);
|
||||||
|
await user.type(input, constructedAttribute);
|
||||||
|
|
||||||
|
const returnedOptions = screen.getAllByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
expect(returnedOptions.length).toEqual(HAVING_OPERATORS.length);
|
||||||
|
|
||||||
|
// check write value after operator
|
||||||
|
await user.clear(input);
|
||||||
|
await user.type(input, `${constructedAttribute} != 123`);
|
||||||
|
|
||||||
|
expect(input).toHaveValue(`${constructedAttribute} != 123`);
|
||||||
|
|
||||||
|
const optionWithValue = screen.getByTitle(optionTestTitle);
|
||||||
|
|
||||||
|
// onChange after complete writting in the input or autocomplete
|
||||||
|
await user.click(optionWithValue);
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onChange).toHaveBeenCalledWith([
|
||||||
|
transformFromStringToHaving(`${constructedAttribute} != 123`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// onChange with multiple operator
|
||||||
|
await user.type(input, `${constructedAttribute} IN 123 123`);
|
||||||
|
|
||||||
|
expect(input).toHaveValue(`${constructedAttribute} IN 123 123`);
|
||||||
|
|
||||||
|
const optionWithMultipleValue = screen.getByTitle(optionTestTitle);
|
||||||
|
await user.click(optionWithMultipleValue);
|
||||||
|
|
||||||
|
expect(onChange).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onChange).toHaveBeenCalledWith([
|
||||||
|
transformFromStringToHaving(`${constructedAttribute} IN 123 123`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { HavingFilter } from './HavingFilter';
|
@ -1,4 +1,5 @@
|
|||||||
export { AggregatorFilter } from './AggregatorFilter';
|
export { AggregatorFilter } from './AggregatorFilter';
|
||||||
export { GroupByFilter } from './GroupByFilter';
|
export { GroupByFilter } from './GroupByFilter';
|
||||||
|
export { HavingFilter } from './HavingFilter';
|
||||||
export { OperatorsSelect } from './OperatorsSelect';
|
export { OperatorsSelect } from './OperatorsSelect';
|
||||||
export { ReduceToFilter } from './ReduceToFilter';
|
export { ReduceToFilter } from './ReduceToFilter';
|
||||||
|
17
frontend/src/lib/newQueryBuilder/createNewBuilderItemName.ts
Normal file
17
frontend/src/lib/newQueryBuilder/createNewBuilderItemName.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
type CreateNewBuilderItemNameParams = {
|
||||||
|
existNames: string[];
|
||||||
|
sourceNames: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createNewBuilderItemName = ({
|
||||||
|
existNames,
|
||||||
|
sourceNames,
|
||||||
|
}: CreateNewBuilderItemNameParams): string => {
|
||||||
|
for (let i = 0; i < sourceNames.length; i += 1) {
|
||||||
|
if (!existNames.includes(sourceNames[i])) {
|
||||||
|
return sourceNames[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
@ -1,9 +0,0 @@
|
|||||||
export const MAX_FORMULAS = 20;
|
|
||||||
|
|
||||||
const currentArray: string[] = Array.from(
|
|
||||||
Array(MAX_FORMULAS),
|
|
||||||
(_, i) => `F${i + 1}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const createNewFormulaName = (index: number): string =>
|
|
||||||
currentArray[index];
|
|
@ -1,14 +0,0 @@
|
|||||||
export const MAX_QUERIES = 26;
|
|
||||||
|
|
||||||
const alpha: number[] = Array.from(Array(MAX_QUERIES), (_, i) => i + 65);
|
|
||||||
const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
|
|
||||||
|
|
||||||
export const createNewQueryName = (existNames: string[]): string => {
|
|
||||||
for (let i = 0; i < alphabet.length; i += 1) {
|
|
||||||
if (!existNames.includes(alphabet[i])) {
|
|
||||||
return alphabet[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
22
frontend/src/lib/query/transformQueryBuilderData.ts
Normal file
22
frontend/src/lib/query/transformQueryBuilderData.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
|
import { Having } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export const transformHavingToStringValue = (having: Having[]): string[] => {
|
||||||
|
const result: string[] = having.map((item) => {
|
||||||
|
const operator = Object.entries(OPERATORS).find(([key]) => key === item.op);
|
||||||
|
|
||||||
|
return `${item.columnName} ${operator ? operator[1] : ''} ${item.value.join(
|
||||||
|
' ',
|
||||||
|
)}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformFromStringToHaving = (havingStr: string): Having => {
|
||||||
|
const [columnName, op, ...value] = havingStr.split(' ');
|
||||||
|
|
||||||
|
const operator = Object.entries(OPERATORS).find(([, value]) => value === op);
|
||||||
|
|
||||||
|
return { columnName, op: operator ? operator[0] : '', value };
|
||||||
|
};
|
@ -1,13 +1,17 @@
|
|||||||
// ** Helpers
|
// ** Helpers
|
||||||
// ** Constants
|
// ** Constants
|
||||||
import {
|
import {
|
||||||
|
initialFormulaBuilderFormValues,
|
||||||
initialQueryBuilderFormValues,
|
initialQueryBuilderFormValues,
|
||||||
mapOfOperators,
|
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import {
|
import {
|
||||||
createNewQueryName,
|
alphabet,
|
||||||
|
formulasNames,
|
||||||
|
mapOfOperators,
|
||||||
|
MAX_FORMULAS,
|
||||||
MAX_QUERIES,
|
MAX_QUERIES,
|
||||||
} from 'lib/newQueryBuilder/createNewQueryName';
|
} from 'constants/queryBuilder';
|
||||||
|
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
@ -37,6 +41,7 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
|||||||
setupInitialDataSource: () => {},
|
setupInitialDataSource: () => {},
|
||||||
removeEntityByIndex: () => {},
|
removeEntityByIndex: () => {},
|
||||||
addNewQuery: () => {},
|
addNewQuery: () => {},
|
||||||
|
addNewFormula: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialQueryBuilderData: QueryBuilderData = {
|
const initialQueryBuilderData: QueryBuilderData = {
|
||||||
@ -93,12 +98,15 @@ export function QueryBuilderProvider({
|
|||||||
|
|
||||||
const newQuery: IBuilderQueryForm = {
|
const newQuery: IBuilderQueryForm = {
|
||||||
...initialQueryBuilderFormValues,
|
...initialQueryBuilderFormValues,
|
||||||
queryName: createNewQueryName(existNames),
|
queryName: createNewBuilderItemName({ existNames, sourceNames: alphabet }),
|
||||||
...(initialDataSource
|
...(initialDataSource
|
||||||
? {
|
? {
|
||||||
dataSource: initialDataSource,
|
dataSource: initialDataSource,
|
||||||
aggregateOperator: mapOfOperators[initialDataSource][0],
|
aggregateOperator: mapOfOperators[initialDataSource][0],
|
||||||
expression: createNewQueryName(existNames),
|
expression: createNewBuilderItemName({
|
||||||
|
existNames,
|
||||||
|
sourceNames: alphabet,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
@ -108,6 +116,17 @@ export function QueryBuilderProvider({
|
|||||||
[initialDataSource],
|
[initialDataSource],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const createNewFormula = useCallback((formulas: IBuilderFormula[]) => {
|
||||||
|
const existNames = formulas.map((item) => item.label);
|
||||||
|
|
||||||
|
const newFormula: IBuilderFormula = {
|
||||||
|
...initialFormulaBuilderFormValues,
|
||||||
|
label: createNewBuilderItemName({ existNames, sourceNames: formulasNames }),
|
||||||
|
};
|
||||||
|
|
||||||
|
return newFormula;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const addNewQuery = useCallback(() => {
|
const addNewQuery = useCallback(() => {
|
||||||
setQueryBuilderData((prevState) => {
|
setQueryBuilderData((prevState) => {
|
||||||
if (prevState.queryData.length >= MAX_QUERIES) return prevState;
|
if (prevState.queryData.length >= MAX_QUERIES) return prevState;
|
||||||
@ -118,6 +137,19 @@ export function QueryBuilderProvider({
|
|||||||
});
|
});
|
||||||
}, [createNewQuery]);
|
}, [createNewQuery]);
|
||||||
|
|
||||||
|
const addNewFormula = useCallback(() => {
|
||||||
|
setQueryBuilderData((prevState) => {
|
||||||
|
if (prevState.queryFormulas.length >= MAX_FORMULAS) return prevState;
|
||||||
|
|
||||||
|
const newFormula = createNewFormula(prevState.queryFormulas);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
queryFormulas: [...prevState.queryFormulas, newFormula],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [createNewFormula]);
|
||||||
|
|
||||||
const setupInitialDataSource = useCallback(
|
const setupInitialDataSource = useCallback(
|
||||||
(newInitialDataSource: DataSource | null) =>
|
(newInitialDataSource: DataSource | null) =>
|
||||||
setInitialDataSource(newInitialDataSource),
|
setInitialDataSource(newInitialDataSource),
|
||||||
@ -133,6 +165,12 @@ export function QueryBuilderProvider({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateFormulaBuilderData = useCallback(
|
||||||
|
(formulas: IBuilderFormula[], index: number, newFormula: IBuilderFormula) =>
|
||||||
|
formulas.map((item, idx) => (index === idx ? newFormula : item)),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSetQueryData = useCallback(
|
const handleSetQueryData = useCallback(
|
||||||
(index: number, newQueryData: IBuilderQueryForm): void => {
|
(index: number, newQueryData: IBuilderQueryForm): void => {
|
||||||
setQueryBuilderData((prevState) => {
|
setQueryBuilderData((prevState) => {
|
||||||
@ -152,10 +190,22 @@ export function QueryBuilderProvider({
|
|||||||
);
|
);
|
||||||
const handleSetFormulaData = useCallback(
|
const handleSetFormulaData = useCallback(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
(index: number, formulaData: IBuilderFormula): void => {},
|
(index: number, formulaData: IBuilderFormula): void => {
|
||||||
[],
|
setQueryBuilderData((prevState) => {
|
||||||
|
const updatedFormulasBuilderData = updateFormulaBuilderData(
|
||||||
|
prevState.queryFormulas,
|
||||||
|
index,
|
||||||
|
formulaData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
queryFormulas: updatedFormulasBuilderData,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateFormulaBuilderData],
|
||||||
);
|
);
|
||||||
console.log(queryBuilderData.queryData);
|
|
||||||
|
|
||||||
const contextValues: QueryBuilderContextType = useMemo(
|
const contextValues: QueryBuilderContextType = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -168,6 +218,7 @@ export function QueryBuilderProvider({
|
|||||||
setupInitialDataSource,
|
setupInitialDataSource,
|
||||||
removeEntityByIndex,
|
removeEntityByIndex,
|
||||||
addNewQuery,
|
addNewQuery,
|
||||||
|
addNewFormula,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
queryBuilderData,
|
queryBuilderData,
|
||||||
@ -179,6 +230,7 @@ export function QueryBuilderProvider({
|
|||||||
setupInitialDataSource,
|
setupInitialDataSource,
|
||||||
removeEntityByIndex,
|
removeEntityByIndex,
|
||||||
addNewQuery,
|
addNewQuery,
|
||||||
|
addNewFormula,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -24,11 +24,11 @@ export interface TagFilter {
|
|||||||
op: string;
|
op: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Having {
|
export type Having = {
|
||||||
key: string;
|
columnName: string;
|
||||||
value: string;
|
|
||||||
op: string;
|
op: string;
|
||||||
}
|
value: string[];
|
||||||
|
};
|
||||||
|
|
||||||
// Type for query builder
|
// Type for query builder
|
||||||
export type IBuilderQuery = {
|
export type IBuilderQuery = {
|
||||||
|
@ -149,4 +149,5 @@ export type QueryBuilderContextType = {
|
|||||||
setupInitialDataSource: (newInitialDataSource: DataSource | null) => void;
|
setupInitialDataSource: (newInitialDataSource: DataSource | null) => void;
|
||||||
removeEntityByIndex: (type: keyof QueryBuilderData, index: number) => void;
|
removeEntityByIndex: (type: keyof QueryBuilderData, index: number) => void;
|
||||||
addNewQuery: () => void;
|
addNewQuery: () => void;
|
||||||
|
addNewFormula: () => void;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user