feat(builder): add additional filter toggler (#2549)

* feat(builder): add additional filter toggler

* feat(builder): add filters deps from another

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
Yevhen Shevchenko 2023-04-10 15:02:45 +03:00 committed by GitHub
parent 9ac2308b89
commit 7c952fd9cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 736 additions and 143 deletions

View File

@ -0,0 +1,63 @@
// ** TODO: use it for creating formula names
// import { createNewFormulaName } from 'lib/newQueryBuilder/createNewFormulaName';
// ** Helpers
import { createNewQueryName } from 'lib/newQueryBuilder/createNewQueryName';
import { LocalDataType } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import {
BoolOperators,
DataSource,
LogsAggregatorOperator,
MetricAggregateOperator,
NumberOperators,
StringOperators,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
export enum QueryBuilderKeys {
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
}
export const mapOfOperators: Record<DataSource, string[]> = {
metrics: Object.values(MetricAggregateOperator),
logs: Object.values(LogsAggregatorOperator),
traces: Object.values(TracesAggregatorOperator),
};
export const mapOfFilters: Record<DataSource, string[]> = {
// eslint-disable-next-line sonarjs/no-duplicate-string
metrics: ['Having', 'Aggregation interval'],
logs: ['Limit', 'Having', 'Order by', 'Aggregation interval'],
traces: ['Limit', 'Having', 'Order by', 'Aggregation interval'],
};
export const initialAggregateAttribute: IBuilderQueryForm['aggregateAttribute'] = {
dataType: null,
key: '',
isColumn: null,
type: null,
};
export const initialQueryBuilderFormValues: IBuilderQueryForm = {
dataSource: DataSource.METRICS,
queryName: createNewQueryName([]),
aggregateOperator: Object.values(MetricAggregateOperator)[0],
aggregateAttribute: initialAggregateAttribute,
tagFilters: [],
expression: '',
disabled: false,
having: [],
stepInterval: 30,
limit: 10,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: '',
};
export const operatorsByTypes: Record<LocalDataType, string[]> = {
string: Object.values(StringOperators),
number: Object.values(NumberOperators),
bool: Object.values(BoolOperators),
};

View File

@ -1,4 +0,0 @@
export enum QueryBuilderKeys {
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
}

View File

@ -169,7 +169,7 @@ function QuerySection({
/>
// TODO: uncomment for testing new QueryBuilder
// <QueryBuilder />
// <QueryBuilder panelType={selectedGraph} />
),
},
{

View File

@ -1,3 +1,4 @@
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { DataSource } from 'types/common/queryBuilder';
export type QueryBuilderConfig =
@ -9,4 +10,5 @@ export type QueryBuilderConfig =
export type QueryBuilderProps = {
config?: QueryBuilderConfig;
panelType?: ITEMS;
};

View File

@ -1,35 +1,87 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Col, Row } from 'antd';
// ** Hooks
import { useQueryBuilder } from 'hooks/useQueryBuilder';
import React from 'react';
import { MAX_FORMULAS } from 'lib/newQueryBuilder/createNewFormulaName';
// ** Constants
import { MAX_QUERIES } from 'lib/newQueryBuilder/createNewQueryName';
import React, { memo, useEffect, useMemo } from 'react';
// ** Components
import { Query } from './components';
// ** Types
import { QueryBuilderProps } from './QueryBuilder.interfaces';
// ** Styles
// TODO: I think it can be components switcher, because if we have different views based on the data source, we can render based on source
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function QueryBuilder({ config }: QueryBuilderProps): JSX.Element {
const { queryBuilderData } = useQueryBuilder();
export const QueryBuilder = memo(function QueryBuilder({
config,
panelType,
}: QueryBuilderProps): JSX.Element {
const {
queryBuilderData,
setupInitialDataSource,
addNewQuery,
} = useQueryBuilder();
// Here we can use Form from antd library and fill context data or edit
// Connect form with adding or removing items from the list
useEffect(() => {
if (config && config.queryVariant === 'static') {
setupInitialDataSource(config.initialDataSource);
}
// Here will be map of query queryBuilderData.queryData and queryBuilderData.queryFormulas components
// Each component can be part of antd Form list where we can add or remove items
// Also need decide to make a copy of queryData for working with form or not and after it set the full new list with formulas or queries to the context
// With button to add him
return (
<div>
{queryBuilderData.queryData.map((query, index) => (
<Query
key={query.queryName}
index={index}
isAvailableToDisable={queryBuilderData.queryData.length > 1}
queryVariant={config?.queryVariant || 'dropdown'}
query={query}
/>
))}
</div>
return (): void => {
setupInitialDataSource(null);
};
}, [config, setupInitialDataSource]);
const isDisabledQueryButton = useMemo(
() => queryBuilderData.queryData.length >= MAX_QUERIES,
[queryBuilderData],
);
}
const isDisabledFormulaButton = useMemo(
() => queryBuilderData.queryData.length >= MAX_FORMULAS,
[queryBuilderData],
);
return (
<Row gutter={[0, 20]} justify="start">
<Col span={24}>
<Row gutter={[0, 50]}>
{queryBuilderData.queryData.map((query, index) => (
<Col key={query.queryName} span={24}>
<Query
index={index}
isAvailableToDisable={queryBuilderData.queryData.length > 1}
queryVariant={config?.queryVariant || 'dropdown'}
query={query}
panelType={panelType}
/>
</Col>
))}
</Row>
</Col>
<Row gutter={[20, 0]}>
<Col>
<Button
disabled={isDisabledQueryButton}
type="primary"
icon={<PlusOutlined />}
onClick={addNewQuery}
>
Query
</Button>
</Col>
<Col>
<Button
disabled={isDisabledFormulaButton}
type="primary"
icon={<PlusOutlined />}
>
Formula
</Button>
</Col>
</Row>
</Row>
);
});

View File

@ -0,0 +1,5 @@
import { PropsWithChildren } from 'react';
export type AdditionalFiltersProps = PropsWithChildren & {
listOfAdditionalFilter: string[];
};

View File

@ -0,0 +1,40 @@
import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import styled, { css } from 'styled-components';
const IconCss = css`
margin-right: 0.6875rem;
transition: all 0.2s ease;
`;
export const StyledIconOpen = styled(PlusSquareOutlined)`
${IconCss}
`;
export const StyledIconClose = styled(MinusSquareOutlined)`
${IconCss}
`;
export const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
width: fit-content;
`;
export const StyledInner = styled.div`
width: 100%;
display: flex;
align-items: center;
margin-bottom: 0.875rem;
min-height: 1.375rem;
cursor: pointer;
&:hover {
${StyledIconOpen}, ${StyledIconClose} {
opacity: 0.7;
}
}
`;
export const StyledLink = styled(Typography.Link)`
pointer-events: none;
`;

View File

@ -0,0 +1,52 @@
import { Row } from 'antd';
import React, { Fragment, memo, ReactNode, useState } from 'react';
// ** Types
import { AdditionalFiltersProps } from './AdditionalFiltersToggler.interfaces';
// ** Styles
import {
StyledIconClose,
StyledIconOpen,
StyledInner,
StyledLink,
StyledWrapper,
} from './AdditionalFiltersToggler.styled';
export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
children,
listOfAdditionalFilter,
}: AdditionalFiltersProps): JSX.Element {
const [isOpenedFilters, setIsOpenedFilters] = useState<boolean>(false);
const handleToggleOpenFilters = (): void => {
setIsOpenedFilters((prevState) => !prevState);
};
const filtersTexts: ReactNode = listOfAdditionalFilter.map((str, index) => {
const isNextLast = index + 1 === listOfAdditionalFilter.length - 1;
if (index === listOfAdditionalFilter.length - 1) {
return (
<Fragment key={str}>
and <StyledLink>{str.toUpperCase()}</StyledLink>
</Fragment>
);
}
return (
<span key={str}>
<StyledLink>{str.toUpperCase()}</StyledLink>
{isNextLast ? ' ' : ', '}
</span>
);
});
return (
<StyledWrapper>
<StyledInner onClick={handleToggleOpenFilters}>
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
{!isOpenedFilters && <span>Add conditions for {filtersTexts}</span>}
</StyledInner>
{isOpenedFilters && <Row>{children}</Row>}
</StyledWrapper>
);
});

View File

@ -0,0 +1 @@
export { AdditionalFiltersToggler } from './AdditionalFiltersToggler';

View File

@ -1,5 +1,5 @@
import { Select } from 'antd';
import React from 'react';
import React, { memo } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
// ** Helpers
@ -10,7 +10,9 @@ import { QueryLabelProps } from './DataSourceDropdown.interfaces';
const dataSourceMap = [DataSource.LOGS, DataSource.METRICS, DataSource.TRACES];
export function DataSourceDropdown(props: QueryLabelProps): JSX.Element {
export const DataSourceDropdown = memo(function DataSourceDropdown(
props: QueryLabelProps,
): JSX.Element {
const { onChange, value, style } = props;
const dataSourceOptions: SelectOption<
@ -30,4 +32,4 @@ export function DataSourceDropdown(props: QueryLabelProps): JSX.Element {
style={style}
/>
);
}
});

View File

@ -5,6 +5,7 @@ export const StyledLabel = styled.div`
width: fit-content;
min-height: 2rem;
display: inline-flex;
white-space: nowrap;
align-items: center;
border-radius: 0.125rem;
border: 0.0625rem solid #434343;

View File

@ -1,10 +1,12 @@
import React from 'react';
import React, { memo } from 'react';
// ** Types
import { FilterLabelProps } from './FilterLabel.interfaces';
// ** Styles
import { StyledLabel } from './FilterLabel.styled';
export function FilterLabel({ label }: FilterLabelProps): JSX.Element {
export const FilterLabel = memo(function FilterLabel({
label,
}: FilterLabelProps): JSX.Element {
return <StyledLabel>{label}</StyledLabel>;
}
});

View File

@ -1,12 +1,13 @@
import { Button } from 'antd';
import styled from 'styled-components';
export const StyledButton = styled(Button)<{ isAvailableToDisable: boolean }>`
export const StyledButton = styled(Button)<{ $isAvailableToDisable: boolean }>`
min-width: 2rem;
height: 2.25rem;
padding: 0.125rem;
padding: ${(props): string =>
props.$isAvailableToDisable ? '0.43rem' : '0.43rem 0.68rem'};
border-radius: 0.375rem;
margin-right: 0.1rem;
pointer-events: ${(props): string =>
props.isAvailableToDisable ? 'default' : 'none'};
props.$isAvailableToDisable ? 'default' : 'none'};
`;

View File

@ -1,13 +1,13 @@
import { EyeFilled, EyeInvisibleFilled } from '@ant-design/icons';
import { ButtonProps } from 'antd';
import React from 'react';
import React, { memo } from 'react';
// ** Types
import { ListMarkerProps } from './ListMarker.interfaces';
// ** Styles
import { StyledButton } from './ListMarker.styled';
export function ListMarker({
export const ListMarker = memo(function ListMarker({
isDisabled,
labelName,
index,
@ -30,10 +30,10 @@ export function ListMarker({
icon={buttonProps.icon}
onClick={buttonProps.onClick}
className={className}
isAvailableToDisable={isAvailableToDisable}
$isAvailableToDisable={isAvailableToDisable}
style={style}
>
{labelName}
</StyledButton>
);
}
});

View File

@ -1,3 +1,4 @@
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
export type QueryProps = {
@ -5,4 +6,5 @@ export type QueryProps = {
isAvailableToDisable: boolean;
query: IBuilderQueryForm;
queryVariant: 'static' | 'dropdown';
panelType?: ITEMS;
};

View File

@ -0,0 +1,22 @@
import { CloseCircleOutlined } from '@ant-design/icons';
import { Row } from 'antd';
import styled from 'styled-components';
export const StyledDeleteEntity = styled(CloseCircleOutlined)`
position: absolute;
top: 0.9375rem;
right: 0.9375rem;
z-index: 1;
cursor: pointer;
opacity: 0.45;
width: 1.3125rem;
height: 1.3125rem;
svg {
width: 100%;
height: 100%;
}
`;
export const StyledRow = styled(Row)`
padding-right: 3rem;
`;

View File

@ -1,7 +1,14 @@
/* eslint-disable react/jsx-props-no-spreading */
import { Col, Row } from 'antd';
import { Col, Input, Row } from 'antd';
// ** Constants
import {
initialAggregateAttribute,
mapOfFilters,
mapOfOperators,
} from 'constants/queryBuilder';
import { initialQueryBuilderFormValues } from 'constants/queryBuilder';
// ** Components
import {
AdditionalFiltersToggler,
DataSourceDropdown,
FilterLabel,
ListMarker,
@ -10,64 +17,178 @@ import {
AggregatorFilter,
GroupByFilter,
OperatorsSelect,
ReduceToFilter,
} from 'container/QueryBuilder/filters';
// Context
import { useQueryBuilder } from 'hooks/useQueryBuilder';
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
// ** Hooks
import React from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
// ** Constants
import {
LogsAggregatorOperator,
MetricAggregateOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { transformToUpperCase } from 'utils/transformToUpperCase';
// ** Types
import { QueryProps } from './Query.interfaces';
// ** Styles
import { StyledDeleteEntity, StyledRow } from './Query.styled';
const mapOfOperators: Record<DataSource, string[]> = {
metrics: Object.values(MetricAggregateOperator),
logs: Object.values(LogsAggregatorOperator),
traces: Object.values(TracesAggregatorOperator),
};
export function Query({
export const Query = memo(function Query({
index,
isAvailableToDisable,
queryVariant,
query,
panelType,
}: QueryProps): JSX.Element {
const { handleSetQueryData } = useQueryBuilder();
const {
handleSetQueryData,
removeEntityByIndex,
initialDataSource,
} = useQueryBuilder();
const currentListOfOperators = mapOfOperators[query.dataSource];
const currentListOfOperators = useMemo(
() => mapOfOperators[query.dataSource],
[query],
);
const listOfAdditionalFilters = useMemo(() => mapOfFilters[query.dataSource], [
query,
]);
const handleChangeOperator = (value: string): void => {
handleSetQueryData(index, { aggregateOperator: value });
};
const handleChangeOperator = useCallback(
(value: string): void => {
const aggregateDataType: BaseAutocompleteData['dataType'] =
query.aggregateAttribute.dataType;
const handleChangeDataSource = (nextSource: DataSource): void => {
handleSetQueryData(index, { dataSource: nextSource });
};
const newQuery: IBuilderQueryForm = {
...query,
aggregateOperator: value,
having: [],
};
const handleToggleDisableQuery = (): void => {
handleSetQueryData(index, { disabled: !query.disabled });
};
if (!aggregateDataType || query.dataSource === DataSource.METRICS) {
handleSetQueryData(index, newQuery);
return;
}
const handleChangeAggregatorAttribute = (
value: BaseAutocompleteData,
): void => {
handleSetQueryData(index, { aggregateAttribute: value });
};
switch (aggregateDataType) {
case 'string':
case 'bool': {
const typeOfValue = findDataTypeOfOperator(value);
const handleChangeGroupByKeys = (values: BaseAutocompleteData[]): void => {
handleSetQueryData(index, { groupBy: values });
};
handleSetQueryData(index, {
...newQuery,
...(typeOfValue === 'number'
? { aggregateAttribute: initialAggregateAttribute }
: {}),
});
break;
}
case 'float64':
case 'int64': {
handleSetQueryData(index, newQuery);
break;
}
default: {
handleSetQueryData(index, newQuery);
break;
}
}
},
[index, query, handleSetQueryData],
);
const handleChangeAggregatorAttribute = useCallback(
(value: BaseAutocompleteData): void => {
const newQuery: IBuilderQueryForm = {
...query,
aggregateAttribute: value,
};
handleSetQueryData(index, newQuery);
},
[index, query, handleSetQueryData],
);
const handleChangeDataSource = useCallback(
(nextSource: DataSource): void => {
let newQuery: IBuilderQueryForm = {
...query,
dataSource: nextSource,
};
if (nextSource !== query.dataSource) {
const initCopy = {
...(initialQueryBuilderFormValues as Partial<IBuilderQueryForm>),
};
delete initCopy.queryName;
newQuery = {
...newQuery,
...initCopy,
dataSource: initialDataSource || nextSource,
aggregateOperator: mapOfOperators[nextSource][0],
};
}
handleSetQueryData(index, newQuery);
},
[index, query, initialDataSource, handleSetQueryData],
);
const handleToggleDisableQuery = useCallback((): void => {
const newQuery: IBuilderQueryForm = {
...query,
disabled: !query.disabled,
};
handleSetQueryData(index, newQuery);
}, [index, query, handleSetQueryData]);
const handleChangeGroupByKeys = useCallback(
(values: BaseAutocompleteData[]): void => {
const newQuery: IBuilderQueryForm = {
...query,
groupBy: values,
};
handleSetQueryData(index, newQuery);
},
[index, query, handleSetQueryData],
);
const handleChangeQueryLegend = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
const newQuery: IBuilderQueryForm = {
...query,
legend: e.target.value,
};
handleSetQueryData(index, newQuery);
},
[index, query, handleSetQueryData],
);
const handleChangeReduceTo = useCallback(
(value: string): void => {
const newQuery: IBuilderQueryForm = {
...query,
reduceTo: value,
};
handleSetQueryData(index, newQuery);
},
[index, query, handleSetQueryData],
);
const handleDeleteQuery = useCallback(() => {
removeEntityByIndex('queryData', index);
}, [removeEntityByIndex, index]);
return (
<Row gutter={[0, 15]}>
<StyledRow gutter={[0, 15]}>
<StyledDeleteEntity onClick={handleDeleteQuery} />
<Col span={24}>
<Row wrap={false} align="middle">
<Col span={24}>
@ -92,14 +213,14 @@ export function Query({
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="95px">
<Col flex="5.93rem">
<OperatorsSelect
value={query.aggregateOperator || currentListOfOperators[0]}
onChange={handleChangeOperator}
operators={currentListOfOperators}
/>
</Col>
<Col flex="1 1 200px">
<Col flex="1 1 12.5rem">
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
@ -109,14 +230,32 @@ export function Query({
</Col>
<Col span={11} offset={2}>
<Row gutter={[11, 5]}>
<Col flex="95px">
<FilterLabel label="Group by" />
<Col flex="5.93rem">
<FilterLabel label={panelType === 'VALUE' ? 'Reduce to' : 'Group by'} />
</Col>
<Col flex="1 1 200px">
<GroupByFilter query={query} onChange={handleChangeGroupByKeys} />
<Col flex="1 1 12.5rem">
{panelType === 'VALUE' ? (
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
) : (
<GroupByFilter query={query} onChange={handleChangeGroupByKeys} />
)}
</Col>
</Row>
</Col>
</Row>
<Col span={24}>
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
{/* TODO: Render filter by Col component */}
test additional filter
</AdditionalFiltersToggler>
</Col>
<Row style={{ width: '100%' }}>
<Input
onChange={handleChangeQueryLegend}
size="middle"
value={query.legend}
addonBefore="Legend Format"
/>
</Row>
</StyledRow>
);
}
});

View File

@ -1,3 +1,4 @@
export { AdditionalFiltersToggler } from './AdditionalFiltersToggler';
export { DataSourceDropdown } from './DataSourceDropdown';
export { FilterLabel } from './FilterLabel';
export { Formula } from './Formula';

View File

@ -2,8 +2,9 @@
import { AutoComplete, Spin } from 'antd';
// ** Api
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
import { initialAggregateAttribute } from 'constants/queryBuilder';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import React, { useMemo, useState } from 'react';
import React, { memo, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { SelectOption } from 'types/common/select';
import { transformToUpperCase } from 'utils/transformToUpperCase';
@ -11,7 +12,7 @@ import { transformToUpperCase } from 'utils/transformToUpperCase';
// ** Types
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
export function AggregatorFilter({
export const AggregatorFilter = memo(function AggregatorFilter({
onChange,
query,
}: AgregatorFilterProps): JSX.Element {
@ -50,7 +51,7 @@ export function AggregatorFilter({
const handleChangeAttribute = (value: string): void => {
const currentAttributeObj = data?.payload?.attributeKeys?.find(
(item) => item.key === value,
) || { key: value, type: null, dataType: null, isColumn: null };
) || { ...initialAggregateAttribute, key: value };
onChange(currentAttributeObj);
};
@ -79,4 +80,4 @@ export function AggregatorFilter({
onChange={handleChangeAttribute}
/>
);
}
});

View File

@ -2,11 +2,11 @@ import { Select, Spin } from 'antd';
// ** Api
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
// ** Constants
import { QueryBuilderKeys } from 'constants/useQueryKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder';
// ** Components
// ** Helpers
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import React, { useState } from 'react';
import React, { memo, useState } from 'react';
import { useQuery } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { SelectOption } from 'types/common/select';
@ -17,7 +17,7 @@ import {
GroupByFilterValue,
} from './GroupByFilter.interfaces';
export function GroupByFilter({
export const GroupByFilter = memo(function GroupByFilter({
query,
onChange,
}: GroupByFilterProps): JSX.Element {
@ -97,4 +97,4 @@ export function GroupByFilter({
onChange={handleChange}
/>
);
}
});

View File

@ -1,5 +1,5 @@
import { Select } from 'antd';
import React from 'react';
import React, { memo } from 'react';
// ** Types
import { SelectOption } from 'types/common/select';
// ** Helpers
@ -7,7 +7,7 @@ import { transformToUpperCase } from 'utils/transformToUpperCase';
import { OperatorsSelectProps } from './OperatorsSelect.interfaces';
export function OperatorsSelect({
export const OperatorsSelect = memo(function OperatorsSelect({
operators,
value,
onChange,
@ -30,4 +30,4 @@ export function OperatorsSelect({
{...props}
/>
);
}
});

View File

@ -0,0 +1,7 @@
import { SelectProps } from 'antd';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
export type ReduceToFilterProps = Omit<SelectProps, 'onChange' | 'value'> & {
query: IBuilderQueryForm;
onChange: (value: string) => void;
};

View File

@ -0,0 +1,26 @@
import { Select } from 'antd';
import React, { memo } from 'react';
// ** Types
import { EReduceOperator } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
import { ReduceToFilterProps } from './ReduceToFilter.interfaces';
export const ReduceToFilter = memo(function ReduceToFilter({
query,
onChange,
}: ReduceToFilterProps): JSX.Element {
const options: SelectOption<string, string>[] = Object.values(
EReduceOperator,
).map((str) => ({ label: str, value: str }));
return (
<Select
placeholder="Reduce to"
style={{ width: '100%' }}
options={options}
value={query.reduceTo}
onChange={onChange}
/>
);
});

View File

@ -0,0 +1 @@
export { ReduceToFilter } from './ReduceToFilter';

View File

@ -1,3 +1,4 @@
export { AggregatorFilter } from './AggregatorFilter';
export { GroupByFilter } from './GroupByFilter';
export { OperatorsSelect } from './OperatorsSelect';
export { ReduceToFilter } from './ReduceToFilter';

View File

@ -0,0 +1,9 @@
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];

View File

@ -0,0 +1,14 @@
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 '';
};

View File

@ -0,0 +1,22 @@
import { operatorsByTypes } from 'constants/queryBuilder';
import { LocalDataType } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const findDataTypeOfOperator = (value: string): LocalDataType | null => {
const entries = Object.entries(operatorsByTypes) as [
LocalDataType,
string[],
][];
for (let i = 0; i < entries.length; i += 1) {
for (let j = 0; j < entries[i][1].length; j += 1) {
const currentOperator = entries[i][1][j];
const type = entries[i][0];
if (currentOperator === value) {
return type;
}
}
}
return null;
};

View File

@ -1,4 +1,11 @@
// ** Helpers
// ** Constants
import { initialQueryBuilderFormValues } from 'constants/queryBuilder';
import { mapOfOperators } from 'constants/queryBuilder';
import {
createNewQueryName,
MAX_QUERIES,
} from 'lib/newQueryBuilder/createNewQueryName';
import React, {
createContext,
PropsWithChildren,
@ -14,17 +21,20 @@ import {
} from 'types/api/queryBuilder/queryBuilderData';
import {
DataSource,
MetricAggregateOperator,
QueryBuilderContextType,
QueryBuilderData,
} from 'types/common/queryBuilder';
export const QueryBuilderContext = createContext<QueryBuilderContextType>({
queryBuilderData: { queryData: [], queryFormulas: [] },
initialDataSource: null,
resetQueryBuilderData: () => {},
handleSetQueryData: () => {},
handleSetFormulaData: () => {},
initQueryBuilderData: () => {},
setupInitialDataSource: () => {},
removeEntityByIndex: () => {},
addNewQuery: () => {},
});
const initialQueryBuilderData: QueryBuilderData = {
@ -35,27 +45,15 @@ const initialQueryBuilderData: QueryBuilderData = {
export function QueryBuilderProvider({
children,
}: PropsWithChildren): JSX.Element {
// ** TODO: get queryId from url for getting data for query builder
// ** TODO: type the params which will be used for request of the data for query builder
// TODO: this is temporary. It will be used when we have fixed dataSource and need create new query with this data source
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [initialDataSource, setInitialDataSource] = useState<DataSource | null>(
null,
);
// TODO: when initialDataSource will be setuped, on create button initial dataSource will from initialDataSource
const [queryBuilderData, setQueryBuilderData] = useState<QueryBuilderData>({
// ** TODO temporary initial value for first query for testing first filters
queryData: [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
{
dataSource: DataSource.METRICS,
queryName: 'A',
aggregateOperator: Object.values(MetricAggregateOperator)[0],
aggregateAttribute: {
dataType: null,
key: '',
isColumn: null,
type: null,
},
groupBy: [],
},
],
queryData: [],
queryFormulas: [],
});
@ -64,7 +62,8 @@ export function QueryBuilderProvider({
setQueryBuilderData(initialQueryBuilderData);
}, []);
// ** Method for setupping query builder data
// ** Method for setuping query builder data
// ** Before setuping transform data from backend to frontend format
const initQueryBuilderData = useCallback(
(queryBuilderData: QueryBuilderData): void => {
setQueryBuilderData(queryBuilderData);
@ -72,44 +71,112 @@ export function QueryBuilderProvider({
[],
);
const handleSetQueryData = useCallback(
(index: number, newQueryData: Partial<IBuilderQueryForm>): void => {
const updatedQueryBuilderData = queryBuilderData.queryData.map((item, idx) =>
index === idx ? { ...item, ...newQueryData } : item,
);
setQueryBuilderData((prevState) => ({
...prevState,
queryData: updatedQueryBuilderData,
}));
const removeEntityByIndex = useCallback(
(type: keyof QueryBuilderData, index: number) => {
setQueryBuilderData((prevState) => {
const currentArray: (IBuilderQueryForm | IBuilderFormula)[] =
prevState[type];
return {
...prevState,
[type]: currentArray.filter((item, i) => index !== i),
};
});
},
[queryBuilderData],
[],
);
const createNewQuery = useCallback(
(queries: IBuilderQueryForm[]): IBuilderQueryForm => {
const existNames = queries.map((item) => item.queryName);
const newQuery: IBuilderQueryForm = {
...initialQueryBuilderFormValues,
queryName: createNewQueryName(existNames),
...(initialDataSource
? {
dataSource: initialDataSource,
aggregateOperator: mapOfOperators[initialDataSource][0],
expression: createNewQueryName(existNames),
}
: {}),
};
return newQuery;
},
[initialDataSource],
);
const addNewQuery = useCallback(() => {
setQueryBuilderData((prevState) => {
if (prevState.queryData.length >= MAX_QUERIES) return prevState;
const newQuery = createNewQuery(prevState.queryData);
return { ...prevState, queryData: [...prevState.queryData, newQuery] };
});
}, [createNewQuery]);
const setupInitialDataSource = useCallback(
(newInitialDataSource: DataSource | null) =>
setInitialDataSource(newInitialDataSource),
[],
);
const updateQueryBuilderData = useCallback(
(
queries: IBuilderQueryForm[],
index: number,
newQueryData: IBuilderQueryForm,
) => queries.map((item, idx) => (index === idx ? newQueryData : item)),
[],
);
const handleSetQueryData = useCallback(
(index: number, newQueryData: IBuilderQueryForm): void => {
setQueryBuilderData((prevState) => {
const updatedQueryBuilderData = updateQueryBuilderData(
prevState.queryData,
index,
newQueryData,
);
return {
...prevState,
queryData: updatedQueryBuilderData,
};
});
},
[updateQueryBuilderData],
);
const handleSetFormulaData = useCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(index: number, formulaData: IBuilderFormula): void => {},
[],
);
// ** TODO: Discuss with Palash how the state of the queryBuilder and queryFormulas
// ** TODO: should be filled from url
// ** TODO: put these values and setter to the context value
console.log(queryBuilderData.queryData);
const contextValues: QueryBuilderContextType = useMemo(
() => ({
queryBuilderData,
initialDataSource,
resetQueryBuilderData,
handleSetQueryData,
handleSetFormulaData,
initQueryBuilderData,
setupInitialDataSource,
removeEntityByIndex,
addNewQuery,
}),
[
queryBuilderData,
initialDataSource,
resetQueryBuilderData,
handleSetQueryData,
handleSetFormulaData,
initQueryBuilderData,
setupInitialDataSource,
removeEntityByIndex,
addNewQuery,
],
);

View File

@ -1,5 +1,9 @@
export type LocalDataType = 'number' | 'string' | 'bool';
export type DataType = 'int64' | 'float64' | 'string' | 'bool';
export interface BaseAutocompleteData {
dataType: 'number' | 'string' | 'boolean' | null;
dataType: DataType | null;
isColumn: boolean | null;
key: string;
type: 'tag' | 'resource' | null;

View File

@ -23,6 +23,12 @@ export interface TagFilter {
op: string;
}
export interface Having {
key: string;
value: string;
op: string;
}
// Type for query builder
export type IBuilderQuery = {
queryName: string;
@ -33,12 +39,14 @@ export type IBuilderQuery = {
groupBy: BaseAutocompleteData[];
expression: string;
disabled: boolean;
having?: string;
limit?: number;
orderBy?: string[];
reduceTo?: string;
having: Having[];
limit: number;
stepInterval: number;
orderBy: string[];
reduceTo: string;
};
export type IBuilderQueryForm = Omit<IBuilderQuery, 'aggregateAttribute'> & {
aggregateAttribute: BaseAutocompleteData;
legend: string;
};

View File

@ -9,6 +9,49 @@ export enum DataSource {
LOGS = 'logs',
}
export enum StringOperators {
NOOP = 'noop',
COUNT = 'count',
COUNT_DISTINCT = 'count_distinct',
}
export enum NumberOperators {
SUM = 'sum',
AVG = 'avg',
MAX = 'max',
MIN = 'min',
P05 = 'p05',
P10 = 'p10',
P20 = 'p20',
P25 = 'p25',
P50 = 'p50',
P75 = 'p75',
P90 = 'p90',
P95 = 'p95',
P99 = 'p99',
RATE = 'rate',
SUM_RATE = 'sum_rate',
AVG_RATE = 'avg_rate',
MAX_RATE = 'max_rate',
MIN_RATE = 'min_rate',
RATE_SUM = 'rate_sum',
RATE_AVG = 'rate_avg',
RATE_MIN = 'rate_min',
RATE_MAX = 'rate_max',
HIST_QUANTILE_50 = 'hist_quantile_50',
HIST_QUANTILE_75 = 'hist_quantile_75',
HIST_QUANTILE_90 = 'hist_quantile_90',
HIST_QUANTILE_95 = 'hist_quantile_95',
HIST_QUANTILE_99 = 'hist_quantile_99',
}
// TODO: add boolean operators from backend
export enum BoolOperators {
NOOP = 'noop',
COUNT = 'count',
COUNT_DISTINCT = 'count_distinct',
}
export enum MetricAggregateOperator {
NOOP = 'noop',
COUNT = 'count',
@ -82,6 +125,14 @@ export enum LogsAggregatorOperator {
RATE = 'rate',
}
export enum EReduceOperator {
LATEST_OF_VALUES_IN_TIMEFRAME = 'Latest of values in timeframe',
'SUM_OF_VALUES_IN_TIMEFRAME' = 'Sum of values in timeframe',
'AVERAGE_OF_VALUES_IN_TIMEFRAME' = 'Average of values in timeframe',
'MAX_OF_VALUES_IN_TIMEFRAME' = 'Max of values in timeframe',
'MIN_OF_VALUES_IN_TIMEFRAME' = 'Min of values in timeframe',
}
export type QueryBuilderData = {
queryData: IBuilderQueryForm[];
queryFormulas: IBuilderFormula[];
@ -90,11 +141,12 @@ export type QueryBuilderData = {
// ** TODO: temporary types for context, fix it during development
export type QueryBuilderContextType = {
queryBuilderData: QueryBuilderData;
initialDataSource: DataSource | null;
resetQueryBuilderData: () => void;
handleSetQueryData: (
index: number,
queryData: Partial<IBuilderQueryForm>,
) => void;
handleSetQueryData: (index: number, queryData: IBuilderQueryForm) => void;
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
initQueryBuilderData: (queryBuilderData: QueryBuilderData) => void;
setupInitialDataSource: (newInitialDataSource: DataSource | null) => void;
removeEntityByIndex: (type: keyof QueryBuilderData, index: number) => void;
addNewQuery: () => void;
};