feat: Connect Query builder with graph (#2611)

* fix: having value data type

* feat: connect new builder to dashboard

* Fix/query builder filters (#2623)

* feat: rename query data type

* fix: remove reset of groupBy

* fix: filters search

* fix: calls autocomplete times

* fix: response mapper

* fix: removee unnecessary field

* fix: no check ts types for old query builder

* fix: disable check utils old builder
This commit is contained in:
Yevhen Shevchenko 2023-05-02 17:08:03 +03:00 committed by GitHub
parent bbda684e65
commit 8c2f33c95a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 1040 additions and 611 deletions

View File

@ -1,17 +1,17 @@
import { ApiV2Instance as axios } from 'api';
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricRangePayloadProps,
MetricRangePayloadV3,
MetricsRangeProps,
} from 'types/api/metrics/getQueryRange';
export const getMetricsQueryRange = async (
props: MetricsRangeProps,
): Promise<SuccessResponse<MetricRangePayloadProps> | ErrorResponse> => {
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
try {
const response = await axios.post(`/metrics/query_range`, props);
const response = await axios.post('/query_range', props);
return {
statusCode: 200,

View File

@ -11,7 +11,6 @@ export const getAggregateKeys = async ({
searchText,
dataSource,
aggregateAttribute,
tagType,
}: IGetAttributeKeysPayload): Promise<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
> => {
@ -19,7 +18,7 @@ export const getAggregateKeys = async ({
const response: AxiosResponse<{
data: IQueryAutocompleteResponse;
}> = await ApiV3Instance.get(
`autocomplete/attribute_keys?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&aggregateAttribute=${aggregateAttribute}&tagType=${tagType}&searchText=${searchText}`,
`autocomplete/attribute_keys?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&aggregateAttribute=${aggregateAttribute}&searchText=${searchText}`,
);
return {

View File

@ -1,10 +1,11 @@
// ** Helpers
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import { LocalDataType } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
Having,
HavingForm,
IBuilderFormula,
IBuilderQueryForm,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BoolOperators,
@ -12,9 +13,13 @@ import {
LogsAggregatorOperator,
MetricAggregateOperator,
NumberOperators,
PanelTypeKeys,
QueryAdditionalFilter,
ReduceOperators,
StringOperators,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
export const MAX_FORMULAS = 20;
export const MAX_QUERIES = 26;
@ -37,45 +42,69 @@ export const mapOfOperators: Record<DataSource, string[]> = {
traces: Object.values(TracesAggregatorOperator),
};
export const mapOfFilters: Record<DataSource, string[]> = {
// eslint-disable-next-line sonarjs/no-duplicate-string
metrics: ['Aggregation interval', 'Having'],
logs: ['Order by', 'Limit', 'Having', 'Aggregation interval'],
traces: ['Order by', 'Limit', 'Having', 'Aggregation interval'],
export const mapOfFilters: Record<DataSource, QueryAdditionalFilter[]> = {
metrics: [
// eslint-disable-next-line sonarjs/no-duplicate-string
{ text: 'Aggregation interval', field: 'stepInterval' },
{ text: 'Having', field: 'having' },
],
logs: [
{ text: 'Order by', field: 'orderBy' },
{ text: 'Limit', field: 'limit' },
{ text: 'Having', field: 'having' },
{ text: 'Aggregation interval', field: 'stepInterval' },
],
traces: [
{ text: 'Order by', field: 'orderBy' },
{ text: 'Limit', field: 'limit' },
{ text: 'Having', field: 'having' },
{ text: 'Aggregation interval', field: 'stepInterval' },
],
};
export const initialHavingValues: Having = {
export const REDUCE_TO_VALUES: SelectOption<ReduceOperators, string>[] = [
{ value: 'last', label: 'Latest of values in timeframe' },
{ value: 'sum', label: 'Sum of values in timeframe' },
{ value: 'avg', label: 'Average of values in timeframe' },
{ value: 'max', label: 'Max of values in timeframe' },
{ value: 'min', label: 'Min of values in timeframe' },
];
export const initialHavingValues: HavingForm = {
columnName: '',
op: '',
value: [],
};
export const initialAggregateAttribute: IBuilderQueryForm['aggregateAttribute'] = {
export const initialAggregateAttribute: IBuilderQuery['aggregateAttribute'] = {
dataType: null,
key: '',
isColumn: null,
type: null,
};
export const initialQueryBuilderFormValues: IBuilderQueryForm = {
export const initialQueryBuilderFormValues: IBuilderQuery = {
dataSource: DataSource.METRICS,
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
aggregateOperator: Object.values(MetricAggregateOperator)[0],
aggregateAttribute: initialAggregateAttribute,
tagFilters: { items: [], op: 'AND' },
expression: '',
expression: createNewBuilderItemName({
existNames: [],
sourceNames: alphabet,
}),
disabled: false,
having: [],
stepInterval: 30,
limit: 10,
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: '',
reduceTo: 'sum',
};
export const initialFormulaBuilderFormValues: IBuilderFormula = {
label: createNewBuilderItemName({
queryName: createNewBuilderItemName({
existNames: [],
sourceNames: formulasNames,
}),
@ -90,6 +119,14 @@ export const operatorsByTypes: Record<LocalDataType, string[]> = {
bool: Object.values(BoolOperators),
};
export const PANEL_TYPES: Record<PanelTypeKeys, GRAPH_TYPES> = {
TIME_SERIES: 'graph',
VALUE: 'value',
TABLE: 'table',
LIST: 'list',
EMPTY_WIDGET: 'EMPTY_WIDGET',
};
export type IQueryBuilderState = 'search';
export const QUERY_BUILDER_SEARCH_VALUES = {
@ -104,22 +141,22 @@ export const OPERATORS = {
NIN: 'NOT_IN',
LIKE: 'LIKE',
NLIKE: 'NOT_LIKE',
EQUALS: '=',
NOT_EQUALS: '!=',
'=': '=',
'!=': '!=',
EXISTS: 'EXISTS',
NOT_EXISTS: 'NOT_EXISTS',
CONTAINS: 'CONTAINS',
NOT_CONTAINS: 'NOT_CONTAINS',
GTE: '>=',
GT: '>',
LTE: '<=',
LT: '<',
'>=': '>=',
'>': '>',
'<=': '<=',
'<': '<',
};
export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
string: [
OPERATORS.EQUALS,
OPERATORS.NOT_EQUALS,
OPERATORS['='],
OPERATORS['!='],
OPERATORS.IN,
OPERATORS.NIN,
OPERATORS.LIKE,
@ -130,48 +167,48 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
OPERATORS.NOT_EXISTS,
],
number: [
OPERATORS.EQUALS,
OPERATORS.NOT_EQUALS,
OPERATORS['='],
OPERATORS['!='],
OPERATORS.IN,
OPERATORS.NIN,
OPERATORS.EXISTS,
OPERATORS.NOT_EXISTS,
OPERATORS.GTE,
OPERATORS.GT,
OPERATORS.LTE,
OPERATORS.LT,
OPERATORS['>='],
OPERATORS['>'],
OPERATORS['<='],
OPERATORS['<'],
],
boolean: [
OPERATORS.EQUALS,
OPERATORS.NOT_EQUALS,
OPERATORS['='],
OPERATORS['!='],
OPERATORS.EXISTS,
OPERATORS.NOT_EXISTS,
],
universal: [
OPERATORS.EQUALS,
OPERATORS.NOT_EQUALS,
OPERATORS['='],
OPERATORS['!='],
OPERATORS.IN,
OPERATORS.NIN,
OPERATORS.EXISTS,
OPERATORS.NOT_EXISTS,
OPERATORS.LIKE,
OPERATORS.NLIKE,
OPERATORS.GTE,
OPERATORS.GT,
OPERATORS.LTE,
OPERATORS.LT,
OPERATORS['>='],
OPERATORS['>'],
OPERATORS['<='],
OPERATORS['<'],
OPERATORS.CONTAINS,
OPERATORS.NOT_CONTAINS,
],
};
export const HAVING_OPERATORS: string[] = [
OPERATORS.EQUALS,
OPERATORS.NOT_EQUALS,
OPERATORS['='],
OPERATORS['!='],
OPERATORS.IN,
OPERATORS.NIN,
OPERATORS.GTE,
OPERATORS.GT,
OPERATORS.LTE,
OPERATORS.LT,
OPERATORS['>='],
OPERATORS['>'],
OPERATORS['<='],
OPERATORS['<'],
];

View File

@ -1,6 +1,7 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { StaticLineProps } from 'components/Graph';
import Spinner from 'components/Spinner';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GridGraphComponent from 'container/GridGraphComponent';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
@ -32,7 +33,7 @@ interface QueryResponseError {
function ChartPreview({
name,
query,
graphType = 'TIME_SERIES',
graphType = PANEL_TYPES.TIME_SERIES,
selectedTime = 'GLOBAL_TIME',
selectedInterval = '5min',
headline,
@ -66,8 +67,8 @@ function ChartPreview({
);
case EQueryType.QUERY_BUILDER:
return (
query.metricsBuilder?.queryBuilder?.length > 0 &&
query.metricsBuilder?.queryBuilder[0].metricName !== ''
query.builder.queryData.length > 0 &&
query.builder.queryData[0].queryName !== ''
);
default:
return false;
@ -85,9 +86,9 @@ function ChartPreview({
query: query || {
queryType: 1,
promQL: [],
metricsBuilder: {
formulas: [],
queryBuilder: [],
builder: {
queryFormulas: [],
queryData: [],
},
clickHouse: [],
},
@ -127,7 +128,7 @@ function ChartPreview({
title={name}
data={chartDataSet}
isStacked
GRAPH_TYPES={graphType || 'TIME_SERIES'}
GRAPH_TYPES={graphType || PANEL_TYPES.TIME_SERIES}
name={name || 'Chart Preview'}
staticLine={staticLine}
/>
@ -137,7 +138,7 @@ function ChartPreview({
}
ChartPreview.defaultProps = {
graphType: 'TIME_SERIES',
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
selectedInterval: '5min',
headline: undefined,

View File

@ -1,5 +1,6 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Tabs } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import MetricsBuilderFormula from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/formula';
import MetricsBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/query';
import {
@ -245,7 +246,7 @@ function QuerySection({
key={key}
queryIndex={key}
queryData={toIMetricsBuilderQuery(current)}
selectedGraph="TIME_SERIES"
selectedGraph={PANEL_TYPES.TIME_SERIES}
handleQueryChange={handleMetricQueryChange}
/>
);

View File

@ -1,3 +1,6 @@
/* eslint-disable */
// @ts-ignore
// @ts-nocheck
import { Time } from 'container/TopNav/DateTimeSelection/config';
import {
IBuilderQueries,
@ -113,6 +116,7 @@ export const prepareStagedQuery = (
return {
queryType: t,
promQL: promList,
// TODO: change it later to actual builder
metricsBuilder: {
formulas: formulaList,
queryBuilder: qbList,

View File

@ -3,6 +3,7 @@ import { ChartData } from 'chart.js';
import Graph, { GraphOnClickHandler, StaticLineProps } from 'components/Graph';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import ValueGraph from 'components/ValueGraph';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import history from 'lib/history';
import React from 'react';
@ -25,7 +26,7 @@ function GridGraphComponent({
const isDashboardPage = location.split('/').length === 3;
if (GRAPH_TYPES === 'TIME_SERIES') {
if (GRAPH_TYPES === PANEL_TYPES.TIME_SERIES) {
return (
<Graph
{...{
@ -45,7 +46,7 @@ function GridGraphComponent({
);
}
if (GRAPH_TYPES === 'VALUE') {
if (GRAPH_TYPES === PANEL_TYPES.VALUE) {
const value = (((data.datasets[0] || []).data || [])[0] || 0) as number;
if (data.datasets.length === 0) {

View File

@ -3,8 +3,8 @@ import updateDashboardApi from 'api/dashboard/update';
import {
ClickHouseQueryTemplate,
PromQLQueryTemplate,
QueryBuilderQueryTemplate,
} from 'constants/dashboard';
import { initialQueryBuilderFormValues } from 'constants/queryBuilder';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import GetQueryName from 'lib/query/GetQueryName';
import { Layout } from 'react-grid-layout';
@ -54,14 +54,9 @@ export const UpdateDashboard = async (
...ClickHouseQueryTemplate,
},
],
metricsBuilder: {
formulas: [],
queryBuilder: [
{
name: GetQueryName([]) || '',
...QueryBuilderQueryTemplate,
},
],
builder: {
queryFormulas: [],
queryData: [initialQueryBuilderFormValues],
},
},
queryData: {

View File

@ -1,3 +1,4 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Widgets } from 'types/api/dashboard/getAll';
import { v4 } from 'uuid';
@ -7,7 +8,7 @@ export const getWidgetQueryBuilder = (query: Widgets['query']): Widgets => ({
isStacked: false,
nullZeroValues: '',
opacity: '0',
panelTypes: 'TIME_SERIES',
panelTypes: PANEL_TYPES.TIME_SERIES,
query,
queryData: {
data: { queryData: [] },

View File

@ -1,3 +1,5 @@
/* eslint-disable */
// @ts-nocheck
import { Col } from 'antd';
import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder';
import {
@ -48,6 +50,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
// TODO: change it later to actual builder
metricsBuilder: databaseCallsRPS({
servicename,
legend,
@ -62,6 +65,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
// TODO: change it later to actual builder
metricsBuilder: databaseCallsAvgDuration({
servicename,
tagFilterItems,

View File

@ -1,3 +1,5 @@
/* eslint-disable */
// @ts-nocheck
import { Col } from 'antd';
import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder';
import {
@ -41,6 +43,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
// TODO: change it later to actual builder
metricsBuilder: externalCallErrorPercent({
servicename,
legend: legend.address,
@ -61,6 +64,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
// TODO: change it later to actual builder
metricsBuilder: externalCallDuration({
servicename,
tagFilterItems,
@ -75,6 +79,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
// TODO: change it later to actual builder
metricsBuilder: externalCallRpsByAddress({
servicename,
legend: legend.address,
@ -90,6 +95,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
// TODO: change it later to actual builder
metricsBuilder: externalCallDurationByAddress({
servicename,
legend: legend.address,

View File

@ -1,3 +1,5 @@
/* eslint-disable */
// @ts-nocheck
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
import Graph from 'components/Graph';
import { QueryParams } from 'constants/query';
@ -84,6 +86,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
// TODO: change it later to actual builder
metricsBuilder: operationPerSec({
servicename,
tagFilterItems,
@ -99,6 +102,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
getWidgetQueryBuilder({
queryType: 1,
promQL: [],
// TODO: change it later to actual builder
metricsBuilder: errorPercentage({
servicename,
tagFilterItems,

View File

@ -1,20 +1,21 @@
import TimeSeries from 'assets/Dashboard/TimeSeries';
import ValueIcon from 'assets/Dashboard/Value';
import { PANEL_TYPES } from 'constants/queryBuilder';
const Items: ItemsProps[] = [
{
name: 'TIME_SERIES',
name: PANEL_TYPES.TIME_SERIES,
Icon: TimeSeries,
display: 'Time Series',
},
{
name: 'VALUE',
name: PANEL_TYPES.VALUE,
Icon: ValueIcon,
display: 'Value',
},
];
export type ITEMS = 'TIME_SERIES' | 'VALUE' | 'EMPTY_WIDGET';
export type ITEMS = 'graph' | 'value' | 'list' | 'table' | 'EMPTY_WIDGET';
interface ItemsProps {
name: ITEMS;

View File

@ -1,3 +1,6 @@
/* eslint-disable */
// TODO: fix it after merge actual functionality
// @ts-nocheck
import { PlusOutlined } from '@ant-design/icons';
import {
QueryBuilderFormulaTemplate,

View File

@ -1,5 +1,6 @@
import { AutoComplete, Col, Input, Row, Select, Spin } from 'antd';
import { getMetricName } from 'api/metrics/getMetricName';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import React, { useEffect, useState } from 'react';
import { IMetricsBuilderQuery } from 'types/api/dashboard/getAll';
@ -142,7 +143,7 @@ function MetricsBuilder({
/>
</Row>
<Row style={{ gap: '3%', marginBottom: '1rem' }}>
{selectedGraph === 'TIME_SERIES' ? (
{selectedGraph === PANEL_TYPES.TIME_SERIES ? (
<>
{' '}
<Select

View File

@ -1,3 +1,6 @@
/* eslint-disable */
// TODO: fix it after merge actual functionality
// @ts-nocheck
import { Query } from 'types/api/dashboard/getAll';
import {

View File

@ -4,6 +4,7 @@ import TextToolTip from 'components/TextToolTip';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import { QueryBuilder } from 'container/QueryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isEqual } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect, useSelector } from 'react-redux';
@ -24,11 +25,9 @@ import { v4 as uuid } from 'uuid';
import {
WIDGET_CLICKHOUSE_QUERY_KEY_NAME,
WIDGET_PROMQL_QUERY_KEY_NAME,
WIDGET_QUERY_BUILDER_QUERY_KEY_NAME,
} from './constants';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
import QueryBuilderQueryContainer from './QueryBuilder/queryBuilder';
import TabHeader from './TabHeader';
import { IHandleUpdatedQuery } from './types';
import { getQueryKey } from './utils/getQueryKey';
@ -39,6 +38,7 @@ function QuerySection({
updateQuery,
selectedGraph,
}: QueryProps): JSX.Element {
const { queryBuilderData, initQueryBuilderData } = useQueryBuilder();
const [localQueryChanges, setLocalQueryChanges] = useState<Query>({} as Query);
const [rctTabKey, setRctTabKey] = useState<
Record<keyof typeof EQueryType, string>
@ -47,9 +47,10 @@ function QuerySection({
CLICKHOUSE: uuid(),
PROM: uuid(),
});
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const { dashboards, isLoadingQueryResult } = useSelector<
AppState,
DashboardReducer
>((state) => state.dashboards);
const [selectedDashboards] = dashboards;
const { search } = useLocation();
const { widgets } = selectedDashboards.data;
@ -68,9 +69,9 @@ function QuerySection({
const { query } = selectedWidget || {};
useEffect(() => {
initQueryBuilderData(query.builder);
setLocalQueryChanges(cloneDeep(query) as Query);
}, [query]);
}, [query, initQueryBuilderData]);
const queryDiff = (
queryA: Query,
queryB: Query,
@ -99,7 +100,10 @@ function QuerySection({
const handleStageQuery = (): void => {
updateQuery({
updatedQuery: localQueryChanges,
updatedQuery: {
...localQueryChanges,
builder: queryBuilderData,
},
widgetId: urlQuery.get('widgetId') || '',
yAxisUnit: selectedWidget.yAxisUnit,
});
@ -155,22 +159,7 @@ function QuerySection({
)}
/>
),
children: (
<QueryBuilderQueryContainer
key={rctTabKey.QUERY_BUILDER}
queryData={localQueryChanges}
updateQueryData={({ updatedQuery }: IHandleUpdatedQuery): void => {
handleLocalQueryUpdate({ updatedQuery });
}}
metricsBuilderQueries={
localQueryChanges[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME]
}
selectedGraph={selectedGraph}
/>
// TODO: uncomment for testing new QueryBuilder
// <QueryBuilder panelType={selectedGraph} />
),
children: <QueryBuilder panelType={selectedGraph} />,
},
{
key: EQueryType.CLICKHOUSE.toString(),
@ -234,7 +223,11 @@ function QuerySection({
text: `This will temporarily save the current query and graph state. This will persist across tab change`,
}}
/>
<Button type="primary" onClick={handleStageQuery}>
<Button
loading={isLoadingQueryResult}
type="primary"
onClick={handleStageQuery}
>
Stage & Run Query
</Button>
</span>

View File

@ -9,7 +9,7 @@ export enum EQueryCategories {
}
export enum EQueryTypeToQueryKeyMapping {
QUERY_BUILDER = 'metricsBuilder',
QUERY_BUILDER = 'builder',
CLICKHOUSE = 'clickHouse',
PROM = 'promQL',
}

View File

@ -10,5 +10,5 @@ export type QueryBuilderConfig =
export type QueryBuilderProps = {
config?: QueryBuilderConfig;
panelType?: ITEMS;
panelType: ITEMS;
};

View File

@ -2,7 +2,7 @@ import { PlusOutlined } from '@ant-design/icons';
import { Button, Col, Row } from 'antd';
import { MAX_FORMULAS, MAX_QUERIES } from 'constants/queryBuilder';
// ** Hooks
import { useQueryBuilder } from 'hooks/useQueryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
// ** Constants
import React, { memo, useEffect, useMemo } from 'react';
@ -19,6 +19,7 @@ export const QueryBuilder = memo(function QueryBuilder({
const {
queryBuilderData,
setupInitialDataSource,
resetQueryBuilderData,
addNewQuery,
addNewFormula,
} = useQueryBuilder();
@ -33,6 +34,13 @@ export const QueryBuilder = memo(function QueryBuilder({
};
}, [config, setupInitialDataSource]);
useEffect(
() => (): void => {
resetQueryBuilderData();
},
[resetQueryBuilderData],
);
const isDisabledQueryButton = useMemo(
() => queryBuilderData.queryData.length >= MAX_QUERIES,
[queryBuilderData],
@ -59,7 +67,7 @@ export const QueryBuilder = memo(function QueryBuilder({
</Col>
))}
{queryBuilderData.queryFormulas.map((formula, index) => (
<Col key={formula.label} span={24}>
<Col key={formula.queryName} span={24}>
<Formula formula={formula} index={index} />
</Col>
))}

View File

@ -23,10 +23,12 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
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>
{listOfAdditionalFilter.length > 1 && 'and'}{' '}
<StyledLink>{str.toUpperCase()}</StyledLink>
</Fragment>
);
}

View File

@ -2,7 +2,7 @@ import { Col, Input } from 'antd';
// ** Components
import { ListItemWrapper, ListMarker } from 'container/QueryBuilder/components';
// ** Hooks
import { useQueryBuilder } from 'hooks/useQueryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import React, { ChangeEvent, useCallback } from 'react';
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
@ -46,7 +46,7 @@ export function Formula({ index, formula }: FormulaProps): JSX.Element {
<ListMarker
isDisabled={formula.disabled}
onDisable={handleToggleDisableFormula}
labelName={formula.label}
labelName={formula.queryName}
index={index}
/>
</Col>

View File

@ -0,0 +1,10 @@
export type HavingFilterTagProps = {
label: React.ReactNode;
value: string;
disabled: boolean;
onClose: (event?: React.MouseEvent<HTMLElement, MouseEvent>) => void;
closable: boolean;
onUpdate: (value: string) => void;
};
export type HavingTagRenderProps = Omit<HavingFilterTagProps, 'onUpdate'>;

View File

@ -0,0 +1,13 @@
import { Tag, Typography } from 'antd';
import styled from 'styled-components';
export const StyledText = styled(Typography.Text)`
cursor: pointer;
`;
export const StyledTag = styled(Tag)`
margin-top: 0.125rem;
margin-bottom: 0.125rem;
padding-left: 0.5rem;
display: flex;
`;

View File

@ -0,0 +1,23 @@
import React from 'react';
import { HavingFilterTagProps } from './HavingFilterTag.interfaces';
import { StyledTag, StyledText } from './HavingFilterTag.styled';
export function HavingFilterTag({
value,
closable,
onClose,
onUpdate,
}: HavingFilterTagProps): JSX.Element {
const handleClick = (): void => {
onUpdate(value);
};
return (
<StyledTag closable={closable} onClose={onClose}>
<StyledText ellipsis onClick={handleClick}>
{value}
</StyledText>
</StyledTag>
);
}

View File

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

View File

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

View File

@ -1,11 +1,6 @@
import { Col, Input, Row } from 'antd';
// ** Constants
import {
initialAggregateAttribute,
initialQueryBuilderFormValues,
mapOfFilters,
mapOfOperators,
} from 'constants/queryBuilder';
import { PANEL_TYPES } from 'constants/queryBuilder';
// ** Components
import {
AdditionalFiltersToggler,
@ -25,17 +20,10 @@ import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryF
import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter';
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useQueryBuilder } from 'hooks/useQueryBuilder';
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryOperations';
// ** Hooks
import React, { memo, useCallback, useMemo } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
Having,
IBuilderQueryForm,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import React, { ChangeEvent, memo, ReactNode, useCallback } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { transformToUpperCase } from 'utils/transformToUpperCase';
// ** Types
@ -49,283 +37,82 @@ export const Query = memo(function Query({
panelType,
}: QueryProps): JSX.Element {
const {
handleSetQueryData,
removeEntityByIndex,
initialDataSource,
} = useQueryBuilder();
operators,
isMetricsDataSource,
listOfAdditionalFilters,
handleChangeAggregatorAttribute,
handleChangeDataSource,
handleChangeQueryData,
handleChangeOperator,
handleDeleteQuery,
} = useQueryOperations({ index, query, panelType });
const currentListOfOperators = useMemo(
() => mapOfOperators[query.dataSource],
[query],
);
const listOfAdditionalFilters = useMemo(() => mapOfFilters[query.dataSource], [
query,
]);
const handleChangeOperator = useCallback(
(value: string): void => {
const aggregateDataType: BaseAutocompleteData['dataType'] =
query.aggregateAttribute.dataType;
const newQuery: IBuilderQueryForm = {
...query,
aggregateOperator: value,
having: [],
limit: null,
tagFilters: { items: [], op: 'AND' },
};
if (!aggregateDataType) {
handleSetQueryData(index, newQuery);
return;
}
switch (aggregateDataType) {
case 'string':
case 'bool': {
const typeOfValue = findDataTypeOfOperator(value);
handleSetQueryData(index, {
...newQuery,
...(typeOfValue === 'number'
? { aggregateAttribute: initialAggregateAttribute }
: {}),
});
break;
}
case 'float64':
case 'int64': {
handleSetQueryData(index, newQuery);
break;
}
default: {
handleSetQueryData(index, newQuery);
break;
}
}
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
},
[index, query, handleSetQueryData],
);
const handleChangeAggregatorAttribute = useCallback(
(value: BaseAutocompleteData): void => {
const newQuery: IBuilderQueryForm = {
...query,
aggregateAttribute: value,
having: [],
};
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 handleChangeHavingFilter = useCallback(
(having: Having[]) => {
const newQuery: IBuilderQueryForm = { ...query, having };
handleSetQueryData(index, newQuery);
},
[index, query, handleSetQueryData],
);
const handleDeleteQuery = useCallback(() => {
removeEntityByIndex('queryData', index);
}, [removeEntityByIndex, index]);
const isMatricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
const handleChangeOrderByKeys = useCallback(
(values: BaseAutocompleteData[]): void => {
const newQuery: IBuilderQueryForm = {
...query,
orderBy: values,
};
handleSetQueryData(index, newQuery);
},
[handleSetQueryData, index, query],
[handleChangeQueryData],
);
const handleChangeLimit = useCallback(
(value: number | null): void => {
const newQuery: IBuilderQueryForm = {
...query,
limit: value,
};
handleSetQueryData(index, newQuery);
(value: IBuilderQuery['limit']) => {
handleChangeQueryData('limit', value);
},
[index, query, handleSetQueryData],
[handleChangeQueryData],
);
const handleChangeAggregateEvery = useCallback(
(value: number): void => {
const newQuery: IBuilderQueryForm = {
...query,
stepInterval: value,
};
handleSetQueryData(index, newQuery);
const handleChangeHavingFilter = useCallback(
(value: IBuilderQuery['having']) => {
handleChangeQueryData('having', value);
},
[index, query, handleSetQueryData],
[handleChangeQueryData],
);
const handleChangeOrderByKeys = useCallback(
(value: IBuilderQuery['orderBy']) => {
handleChangeQueryData('orderBy', value);
},
[handleChangeQueryData],
);
const handleToggleDisableQuery = useCallback(() => {
handleChangeQueryData('disabled', !query.disabled);
}, [handleChangeQueryData, query]);
const handleChangeTagFilters = useCallback(
(value: TagFilter): void => {
const newQuery: IBuilderQueryForm = {
...query,
tagFilters: value,
};
handleSetQueryData(index, newQuery);
(value: IBuilderQuery['tagFilters']) => {
handleChangeQueryData('tagFilters', value);
},
[index, query, handleSetQueryData],
[handleChangeQueryData],
);
return (
<ListItemWrapper onDelete={handleDeleteQuery}>
<Col span={24}>
<Row align="middle">
<Col>
<ListMarker
isDisabled={query.disabled}
onDisable={handleToggleDisableQuery}
labelName={query.queryName}
index={index}
isAvailableToDisable={isAvailableToDisable}
/>
{queryVariant === 'dropdown' ? (
<DataSourceDropdown
onChange={handleChangeDataSource}
value={query.dataSource}
style={{ marginRight: '0.5rem' }}
/>
) : (
<FilterLabel label={transformToUpperCase(query.dataSource)} />
)}
</Col>
<Col flex="1">
<Row gutter={[11, 5]}>
{isMatricsDataSource && (
<Col>
<FilterLabel label="WHERE" />
</Col>
)}
<Col flex="1">
<QueryBuilderSearch query={query} onChange={handleChangeTagFilters} />
</Col>
</Row>
</Col>
</Row>
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<OperatorsSelect
value={query.aggregateOperator || currentListOfOperators[0]}
onChange={handleChangeOperator}
operators={currentListOfOperators}
/>
</Col>
<Col flex="1 1 12.5rem">
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
/>
</Col>
</Row>
</Col>
const handleChangeReduceTo = useCallback(
(value: IBuilderQuery['reduceTo']) => {
handleChangeQueryData('reduceTo', value);
},
[handleChangeQueryData],
);
<Col span={11} offset={2}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label={panelType === 'VALUE' ? 'Reduce to' : 'Group by'} />
</Col>
<Col flex="1 1 12.5rem">
{panelType === 'VALUE' ? (
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
) : (
<GroupByFilter query={query} onChange={handleChangeGroupByKeys} />
)}
</Col>
</Row>
</Col>
<Col span={24}>
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
<Row gutter={[0, 11]} justify="space-between">
{!isMatricsDataSource && (
const handleChangeGroupByKeys = useCallback(
(value: IBuilderQuery['groupBy']) => {
handleChangeQueryData('groupBy', value);
},
[handleChangeQueryData],
);
const handleChangeQueryLegend = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
handleChangeQueryData('legend', event.target.value);
},
[handleChangeQueryData],
);
const renderAdditionalFilters = useCallback((): ReactNode => {
switch (panelType) {
case PANEL_TYPES.TIME_SERIES: {
return (
<>
{!isMetricsDataSource && (
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
@ -337,19 +124,17 @@ export const Query = memo(function Query({
</Row>
</Col>
)}
{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>
)}
{!isMatricsDataSource && (
<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>
{!isMetricsDataSource && (
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
@ -375,6 +160,132 @@ export const Query = memo(function Query({
</Col>
</Row>
</Col>
</>
);
}
case PANEL_TYPES.VALUE: {
return (
<>
<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 span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="Aggregate Every" />
</Col>
<Col flex="1 1 6rem">
<AggregateEveryFilter
query={query}
onChange={handleChangeAggregateEvery}
/>
</Col>
</Row>
</Col>
</>
);
}
default: {
return null;
}
}
}, [
panelType,
query,
isMetricsDataSource,
handleChangeAggregateEvery,
handleChangeHavingFilter,
handleChangeLimit,
handleChangeOrderByKeys,
]);
return (
<ListItemWrapper onDelete={handleDeleteQuery}>
<Col span={24}>
<Row align="middle">
<Col>
<ListMarker
isDisabled={query.disabled}
onDisable={handleToggleDisableQuery}
labelName={query.queryName}
index={index}
isAvailableToDisable={isAvailableToDisable}
/>
{queryVariant === 'dropdown' ? (
<DataSourceDropdown
onChange={handleChangeDataSource}
value={query.dataSource}
style={{ marginRight: '0.5rem', minWidth: '5.625rem' }}
/>
) : (
<FilterLabel label={transformToUpperCase(query.dataSource)} />
)}
</Col>
<Col flex="1">
<Row gutter={[11, 5]}>
{isMetricsDataSource && (
<Col>
<FilterLabel label="WHERE" />
</Col>
)}
<Col flex="1">
<QueryBuilderSearch query={query} onChange={handleChangeTagFilters} />
</Col>
</Row>
</Col>
</Row>
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<OperatorsSelect
value={query.aggregateOperator}
onChange={handleChangeOperator}
operators={operators}
/>
</Col>
<Col flex="1 1 12.5rem">
<AggregatorFilter
onChange={handleChangeAggregatorAttribute}
query={query}
/>
</Col>
</Row>
</Col>
<Col span={11} offset={2}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel
label={panelType === PANEL_TYPES.VALUE ? 'Reduce to' : 'Group by'}
/>
</Col>
<Col flex="1 1 12.5rem">
{panelType === PANEL_TYPES.VALUE ? (
<ReduceToFilter query={query} onChange={handleChangeReduceTo} />
) : (
<GroupByFilter
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
query={query}
onChange={handleChangeGroupByKeys}
/>
)}
</Col>
</Row>
</Col>
<Col span={24}>
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
<Row gutter={[0, 11]} justify="space-between">
{renderAdditionalFilters()}
</Row>
</AdditionalFiltersToggler>
</Col>

View File

@ -2,6 +2,7 @@ export { AdditionalFiltersToggler } from './AdditionalFiltersToggler';
export { DataSourceDropdown } from './DataSourceDropdown';
export { FilterLabel } from './FilterLabel';
export { Formula } from './Formula';
export { HavingFilterTag } from './HavingFilterTag';
export { ListItemWrapper } from './ListItemWrapper';
export { ListMarker } from './ListMarker';
export { Query } from './Query';

View File

@ -3,7 +3,7 @@ import getStep from 'lib/getStep';
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { selectStyle } from '../QueryBuilderSearch/config';
@ -47,7 +47,7 @@ function AggregateEveryFilter({
placeholder="Enter in seconds"
disabled={!query.aggregateAttribute.key}
style={selectStyle}
defaultValue={stepInterval ?? query.stepInterval}
defaultValue={query.stepInterval ?? stepInterval}
onChange={(event): void => onChange(Number(event.target.value))}
onKeyDown={handleKeyDown}
/>
@ -56,7 +56,7 @@ function AggregateEveryFilter({
interface AggregateEveryFilterProps {
onChange: (values: number) => void;
query: IBuilderQueryForm;
query: IBuilderQuery;
}
export default AggregateEveryFilter;

View File

@ -1,7 +1,7 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type AgregatorFilterProps = {
onChange: (value: BaseAutocompleteData) => void;
query: IBuilderQueryForm;
query: IBuilderQuery;
};

View File

@ -6,6 +6,7 @@ import { initialAggregateAttribute } from 'constants/queryBuilder';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import React, { memo, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
import { transformToUpperCase } from 'utils/transformToUpperCase';
@ -53,6 +54,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
(item) => item.key === value,
) || { ...initialAggregateAttribute, key: value };
setSearchText('');
onChange(currentAttributeObj);
};
@ -66,10 +68,15 @@ export const AggregatorFilter = memo(function AggregatorFilter({
[query],
);
const placeholder: string =
query.dataSource === DataSource.METRICS
? `${transformToUpperCase(query.dataSource)} name`
: 'Aggregate attribute';
return (
<AutoComplete
showSearch
placeholder={`${transformToUpperCase(query.dataSource)} name`}
placeholder={placeholder}
style={selectStyle}
showArrow={false}
filterOption={false}

View File

@ -1,9 +1,10 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type GroupByFilterProps = {
query: IBuilderQueryForm;
query: IBuilderQuery;
onChange: (values: BaseAutocompleteData[]) => void;
disabled: boolean;
};
export type GroupByFilterValue = {

View File

@ -5,10 +5,9 @@ import { QueryBuilderKeys } from 'constants/queryBuilder';
// ** Components
// ** Helpers
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import React, { memo, useMemo, useState } from 'react';
import React, { memo, useState } from 'react';
import { useQuery } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { MetricAggregateOperator } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
import { selectStyle } from '../QueryBuilderSearch/config';
@ -20,26 +19,35 @@ import {
export const GroupByFilter = memo(function GroupByFilter({
query,
onChange,
disabled,
}: GroupByFilterProps): JSX.Element {
const [searchText, setSearchText] = useState<string>('');
const [isFocused, setIsFocused] = useState<boolean>(false);
const { data, isFetching } = useQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText, isFocused],
async () =>
getAggregateKeys({
aggregateAttribute: query.aggregateAttribute.key,
tagType: query.aggregateAttribute.type,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
searchText,
}),
{ enabled: !!query.aggregateAttribute.key, keepPreviousData: true },
{ enabled: !disabled && isFocused, keepPreviousData: true },
);
const handleSearchKeys = (searchText: string): void => {
setSearchText(searchText);
};
const onBlur = (): void => {
setIsFocused(false);
};
const onFocus = (): void => {
setIsFocused(true);
};
const optionsData: SelectOption<string, string>[] =
data?.payload?.attributeKeys?.map((item) => ({
label: transformStringWithPrefix({
@ -52,10 +60,20 @@ export const GroupByFilter = memo(function GroupByFilter({
const handleChange = (values: GroupByFilterValue[]): void => {
const groupByValues: BaseAutocompleteData[] = values.map((item) => {
const iterationArray = data?.payload?.attributeKeys || query.groupBy;
const existGroup = iterationArray.find((group) => group.key === item.value);
if (existGroup) {
return existGroup;
const responseKeys = data?.payload?.attributeKeys || [];
const existGroupResponse = responseKeys.find(
(group) => group.key === item.value,
);
if (existGroupResponse) {
return existGroupResponse;
}
const existGroupQuery = query.groupBy.find(
(group) => group.key === item.value,
);
if (existGroupQuery) {
return existGroupQuery;
}
return {
@ -66,6 +84,7 @@ export const GroupByFilter = memo(function GroupByFilter({
};
});
setSearchText('');
onChange(groupByValues);
};
@ -81,21 +100,16 @@ export const GroupByFilter = memo(function GroupByFilter({
title: undefined,
}));
const isDisabledSelect = useMemo(
() =>
!query.aggregateAttribute.key ||
query.aggregateOperator === MetricAggregateOperator.NOOP,
[query.aggregateAttribute.key, query.aggregateOperator],
);
return (
<Select
mode="tags"
style={selectStyle}
onSearch={handleSearchKeys}
showSearch
disabled={isDisabledSelect}
disabled={disabled}
showArrow={false}
onBlur={onBlur}
onFocus={onFocus}
filterOption={false}
options={optionsData}
labelInValue

View File

@ -1,9 +1,6 @@
import {
Having,
IBuilderQueryForm,
} from 'types/api/queryBuilder/queryBuilderData';
import { Having, IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type HavingFilterProps = {
query: IBuilderQueryForm;
query: IBuilderQuery;
onChange: (having: Having[]) => void;
};

View File

@ -1,6 +1,8 @@
import { Select } from 'antd';
// ** Constants
import { HAVING_OPERATORS, initialHavingValues } from 'constants/queryBuilder';
import { HavingFilterTag } from 'container/QueryBuilder/components';
import { HavingTagRenderProps } from 'container/QueryBuilder/components/HavingFilterTag/HavingFilterTag.interfaces';
// ** Hooks
import { useTagValidation } from 'hooks/queryBuilder/useTagValidation';
import {
@ -10,7 +12,7 @@ import {
// ** Helpers
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Having } from 'types/api/queryBuilder/queryBuilderData';
import { Having, HavingForm } from 'types/api/queryBuilder/queryBuilderData';
import { SelectOption } from 'types/common/select';
// ** Types
@ -26,7 +28,7 @@ export function HavingFilter({
const [searchText, setSearchText] = useState<string>('');
const [options, setOptions] = useState<SelectOption<string, string>[]>([]);
const [localValues, setLocalValues] = useState<string[]>([]);
const [currentFormValue, setCurrentFormValue] = useState<Having>(
const [currentFormValue, setCurrentFormValue] = useState<HavingForm>(
initialHavingValues,
);
@ -56,7 +58,7 @@ export function HavingFilter({
[columnName],
);
const getHavingObject = useCallback((currentSearch: string): Having => {
const getHavingObject = useCallback((currentSearch: string): HavingForm => {
const textArr = currentSearch.split(' ');
const [columnName = '', op = '', ...value] = textArr;
@ -94,48 +96,92 @@ export function HavingFilter({
[columnName, aggregatorOptions],
);
const isValidHavingValue = (search: string): boolean => {
const values = getHavingObject(search).value.join(' ');
if (values) {
const numRegexp = /^[^a-zA-Z]*$/;
const isValidHavingValue = useCallback(
(search: string): boolean => {
const values = getHavingObject(search).value.join(' ');
return numRegexp.test(values);
}
if (values) {
const numRegexp = /^[-\d.,\s]+$/;
return true;
};
return numRegexp.test(values);
}
const handleSearch = (search: string): void => {
const trimmedSearch = search.replace(/\s\s+/g, ' ').trimStart();
return true;
},
[getHavingObject],
);
const currentSearch = isMulti
? trimmedSearch
: trimmedSearch.split(' ').slice(0, 3).join(' ');
const isValidSearch = isValidHavingValue(currentSearch);
const handleSearch = useCallback(
(search: string): void => {
const trimmedSearch = search.replace(/\s\s+/g, ' ').trimStart();
if (isValidSearch) {
setSearchText(currentSearch);
}
};
const currentSearch = isMulti
? trimmedSearch
: trimmedSearch.split(' ').slice(0, 3).join(' ');
const resetChanges = (): void => {
handleSearch('');
const isValidSearch = isValidHavingValue(currentSearch);
if (isValidSearch) {
setSearchText(currentSearch);
}
},
[isMulti, isValidHavingValue],
);
const resetChanges = useCallback((): void => {
setSearchText('');
setCurrentFormValue(initialHavingValues);
setOptions(aggregatorOptions);
};
}, [aggregatorOptions]);
const handleChange = (values: string[]): void => {
const having: Having[] = values.map(transformFromStringToHaving);
const handleChange = useCallback(
(values: string[]): void => {
const having: Having[] = values.map(transformFromStringToHaving);
const isSelectable: boolean =
currentFormValue.value.length > 0 &&
currentFormValue.value.every((value) => !!value);
const isSelectable =
currentFormValue.value.length > 0 &&
currentFormValue.value.every((value) => !!value);
if (isSelectable) {
onChange(having);
resetChanges();
}
},
[currentFormValue, resetChanges, onChange],
);
const handleUpdateTag = useCallback(
(value: string) => {
const filteredValues = localValues.filter(
(currentValue) => currentValue !== value,
);
const having: Having[] = filteredValues.map(transformFromStringToHaving);
if (isSelectable) {
onChange(having);
resetChanges();
}
};
setSearchText(value);
},
[localValues, onChange],
);
const tagRender = useCallback(
({ label, value, closable, disabled, onClose }: HavingTagRenderProps) => {
const handleClose = (): void => {
onClose();
setSearchText('');
};
return (
<HavingFilterTag
label={label}
value={value}
closable={closable}
disabled={disabled}
onClose={handleClose}
onUpdate={handleUpdateTag}
/>
);
},
[handleUpdateTag],
);
const handleSelect = (currentValue: string): void => {
const { columnName, op, value } = getHavingObject(currentValue);
@ -144,7 +190,7 @@ export function HavingFilter({
const isClearSearch = isCompletedValue && columnName && op;
handleSearch(isClearSearch ? '' : currentValue);
setSearchText(isClearSearch ? '' : currentValue);
};
const parseSearchText = useCallback(
@ -172,9 +218,11 @@ export function HavingFilter({
return (
<Select
autoClearSearchValue={false}
mode="multiple"
onSearch={handleSearch}
searchValue={searchText}
tagRender={tagRender}
value={localValues}
data-testid="havingSelect"
disabled={!query.aggregateAttribute.key}
@ -182,7 +230,6 @@ export function HavingFilter({
notFoundContent={currentFormValue.value.length === 0 ? undefined : null}
placeholder="Count(operation) > 5"
onDeselect={handleDeselect}
onBlur={resetChanges}
onChange={handleChange}
onSelect={handleSelect}
>

View File

@ -8,13 +8,13 @@ import {
import { transformFromStringToHaving } from 'lib/query/transformQueryBuilderData';
import React from 'react';
// ** Types
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
// ** Components
import { HavingFilter } from '../HavingFilter';
const valueWithAttributeAndOperator: IBuilderQueryForm = {
const valueWithAttributeAndOperator: IBuilderQuery = {
...initialQueryBuilderFormValues,
dataSource: DataSource.LOGS,
aggregateOperator: 'SUM',

View File

@ -1,6 +1,6 @@
import { InputNumber } from 'antd';
import React from 'react';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { selectStyle } from '../QueryBuilderSearch/config';
@ -24,6 +24,7 @@ function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
<InputNumber
min={1}
type="number"
defaultValue={query.limit ?? 1}
disabled={!query.aggregateAttribute.key}
style={selectStyle}
onChange={onChange}
@ -34,7 +35,7 @@ function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
interface LimitFilterProps {
onChange: (values: number | null) => void;
query: IBuilderQueryForm;
query: IBuilderQuery;
}
export default LimitFilter;

View File

@ -27,6 +27,7 @@ export const OperatorsSelect = memo(function OperatorsSelect({
value={value}
onChange={onChange}
style={selectStyle}
showSearch
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>

View File

@ -1,8 +1,8 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type OrderByFilterProps = {
query: IBuilderQueryForm;
query: IBuilderQuery;
onChange: (values: BaseAutocompleteData[]) => void;
};

View File

@ -26,7 +26,6 @@ export function OrderByFilter({
async () =>
getAggregateKeys({
aggregateAttribute: query.aggregateAttribute.key,
tagType: query.aggregateAttribute.type,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
searchText,

View File

@ -1 +1 @@
export const selectStyle = { width: '100%' };
export const selectStyle = { width: '100%', minWidth: '10rem' };

View File

@ -2,7 +2,7 @@ import { Select, Spin, Tag, Tooltip } from 'antd';
import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete';
import React, { useEffect, useMemo } from 'react';
import {
IBuilderQueryForm,
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
@ -119,7 +119,7 @@ function QueryBuilderSearch({
}
interface QueryBuilderSearchProps {
query: IBuilderQueryForm;
query: IBuilderQuery;
onChange: (value: TagFilter) => void;
}

View File

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

View File

@ -1,7 +1,8 @@
import { Select } from 'antd';
import { REDUCE_TO_VALUES } from 'constants/queryBuilder';
import React, { memo } from 'react';
// ** Types
import { EReduceOperator } from 'types/common/queryBuilder';
import { ReduceOperators } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
import { ReduceToFilterProps } from './ReduceToFilter.interfaces';
@ -10,17 +11,24 @@ 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 }));
const currentValue =
REDUCE_TO_VALUES.find((option) => option.value === query.reduceTo) ||
REDUCE_TO_VALUES[0];
const handleChange = (
newValue: SelectOption<ReduceOperators, string>,
): void => {
onChange(newValue.value);
};
return (
<Select
placeholder="Reduce to"
style={{ width: '100%' }}
options={options}
value={query.reduceTo}
onChange={onChange}
options={REDUCE_TO_VALUES}
value={currentValue}
labelInValue
onChange={handleChange}
/>
);
});

View File

@ -1,7 +1,7 @@
import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { Option } from 'container/QueryBuilder/type';
import { useCallback, useState } from 'react';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { checkStringEndsWithSpace } from 'utils/checkStringEndsWithSpace';
import { useFetchKeysAndValues } from './useFetchKeysAndValues';
@ -23,7 +23,7 @@ interface IAutoComplete {
isFetching: boolean;
}
export const useAutoComplete = (query: IBuilderQueryForm): IAutoComplete => {
export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>('');
const handleSearch = (value: string): void => setSearchValue(value);
@ -48,6 +48,7 @@ export const useAutoComplete = (query: IBuilderQueryForm): IAutoComplete => {
isValidTag,
isFreeText,
handleSearch,
query,
);
const handleSelect = useCallback(

View File

@ -6,7 +6,7 @@ import {
import { useEffect, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useDebounce } from 'react-use';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { separateSearchValue } from 'utils/separateSearchValue';
type UseFetchKeysAndValuesReturnValues = {
@ -24,7 +24,7 @@ type UseFetchKeysAndValuesReturnValues = {
export const useFetchKeysAndValues = (
searchValue: string,
query: IBuilderQueryForm,
query: IBuilderQuery,
): UseFetchKeysAndValuesReturnValues => {
const [keys, setKeys] = useState<AttributeKeyOptions[]>([]);
const [results, setResults] = useState<string[]>([]);
@ -53,7 +53,7 @@ export const useFetchKeysAndValues = (
*/
const handleFetchOption = async (
value: string,
query: IBuilderQueryForm,
query: IBuilderQuery,
): Promise<void> => {
if (value) {
// separate the search value into the attribute key and the operator

View File

@ -11,16 +11,16 @@ const operatorTypeMapper: Record<string, OperatorType> = {
[OPERATORS.NIN]: 'MULTIPLY_VALUE',
[OPERATORS.EXISTS]: 'NON_VALUE',
[OPERATORS.NOT_EXISTS]: 'NON_VALUE',
[OPERATORS.LTE]: 'SINGLE_VALUE',
[OPERATORS.LT]: 'SINGLE_VALUE',
[OPERATORS.GTE]: 'SINGLE_VALUE',
[OPERATORS.GT]: 'SINGLE_VALUE',
[OPERATORS['<=']]: 'SINGLE_VALUE',
[OPERATORS['<']]: 'SINGLE_VALUE',
[OPERATORS['>=']]: 'SINGLE_VALUE',
[OPERATORS['>']]: 'SINGLE_VALUE',
[OPERATORS.LIKE]: 'SINGLE_VALUE',
[OPERATORS.NLIKE]: 'SINGLE_VALUE',
[OPERATORS.CONTAINS]: 'SINGLE_VALUE',
[OPERATORS.NOT_CONTAINS]: 'SINGLE_VALUE',
[OPERATORS.EQUALS]: 'SINGLE_VALUE',
[OPERATORS.NOT_EQUALS]: 'SINGLE_VALUE',
[OPERATORS['=']]: 'SINGLE_VALUE',
[OPERATORS['!=']]: 'SINGLE_VALUE',
};
export const useOperatorType = (operator: string): OperatorType =>

View File

@ -0,0 +1,164 @@
import {
initialAggregateAttribute,
initialQueryBuilderFormValues,
mapOfFilters,
mapOfOperators,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
HandleChangeQueryData,
UseQueryOperations,
} from 'types/common/operations.types';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
export const useQueryOperations: UseQueryOperations = ({
query,
index,
panelType,
}) => {
const { handleSetQueryData, removeEntityByIndex } = useQueryBuilder();
const [operators, setOperators] = useState<string[]>([]);
const [listOfAdditionalFilters, setListOfAdditionalFilters] = useState<
string[]
>([]);
const { dataSource, aggregateOperator } = query;
const handleChangeOperator = useCallback(
(value: string): void => {
const aggregateDataType: BaseAutocompleteData['dataType'] =
query.aggregateAttribute.dataType;
const typeOfValue = findDataTypeOfOperator(value);
const shouldResetAggregateAttribute =
(aggregateDataType === 'string' || aggregateDataType === 'bool') &&
typeOfValue === 'number';
const newQuery: IBuilderQuery = {
...query,
aggregateOperator: value,
having: [],
orderBy: [],
limit: null,
tagFilters: { items: [], op: 'AND' },
...(shouldResetAggregateAttribute
? { aggregateAttribute: initialAggregateAttribute }
: {}),
};
handleSetQueryData(index, newQuery);
},
[index, query, handleSetQueryData],
);
const getNewOperators = useCallback(
(dataSource: DataSource, currentPanelType: ITEMS): string[] => {
let operatorsByDataSource = mapOfOperators[dataSource];
if (
dataSource !== DataSource.METRICS &&
currentPanelType !== PANEL_TYPES.LIST
) {
operatorsByDataSource = operatorsByDataSource.filter(
(operator) => operator !== StringOperators.NOOP,
);
}
return operatorsByDataSource;
},
[],
);
const getNewListOfAdditionalFilters = useCallback(
(dataSource: DataSource): string[] =>
mapOfFilters[dataSource].map((item) => item.text),
[],
);
const handleChangeAggregatorAttribute = useCallback(
(value: BaseAutocompleteData): void => {
const newQuery: IBuilderQuery = {
...query,
aggregateAttribute: value,
having: [],
};
handleSetQueryData(index, newQuery);
},
[index, query, handleSetQueryData],
);
const handleChangeDataSource = useCallback(
(nextSource: DataSource): void => {
const newOperators = getNewOperators(nextSource, panelType);
const entries = Object.entries(initialQueryBuilderFormValues).filter(
([key]) => key !== 'queryName' && key !== 'expression',
);
const initCopyResult = Object.fromEntries(entries);
const newQuery: IBuilderQuery = {
...query,
...initCopyResult,
dataSource: nextSource,
aggregateOperator: newOperators[0],
};
setOperators(newOperators);
handleSetQueryData(index, newQuery);
},
[index, query, panelType, handleSetQueryData, getNewOperators],
);
const handleDeleteQuery = useCallback(() => {
removeEntityByIndex('queryData', index);
}, [removeEntityByIndex, index]);
const handleChangeQueryData: HandleChangeQueryData = useCallback(
(key, value) => {
const newQuery: IBuilderQuery = {
...query,
[key]: value,
};
handleSetQueryData(index, newQuery);
},
[query, index, handleSetQueryData],
);
const isMetricsDataSource = useMemo(
() => query.dataSource === DataSource.METRICS,
[query.dataSource],
);
useEffect(() => {
if (operators.length === 0) {
const initialOperators = getNewOperators(dataSource, panelType);
setOperators(initialOperators);
}
}, [operators, dataSource, panelType, getNewOperators]);
useEffect(() => {
const additionalFilters = getNewListOfAdditionalFilters(dataSource);
setListOfAdditionalFilters(additionalFilters);
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
return {
isMetricsDataSource,
operators,
listOfAdditionalFilters,
handleChangeOperator,
handleChangeAggregatorAttribute,
handleChangeDataSource,
handleDeleteQuery,
handleChangeQueryData,
};
};

View File

@ -1,5 +1,9 @@
import { isExistsNotExistsOperator } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useCallback, useState } from 'react';
import {
isExistsNotExistsOperator,
isInNotInOperator,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
type IUseTag = {
handleAddTag: (value: string) => void;
@ -21,6 +25,7 @@ export const useTag = (
isValidTag: boolean,
isFreeText: boolean,
handleSearch: (value: string) => void,
query: IBuilderQuery,
): IUseTag => {
const [tags, setTags] = useState<string[]>([]);
@ -55,5 +60,16 @@ export const useTag = (
setTags((prevTags) => prevTags.filter((v) => v !== value));
}, []);
useEffect(() => {
setTags(
(query?.tagFilters?.items || []).map((obj) =>
isInNotInOperator(obj.op)
? `${obj.key} ${obj.op} ${obj.value.join(',')}`
: `${obj.key} ${obj.op} ${obj.value.join(' ')}`,
),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return { handleAddTag, handleClearTag, tags, updateTag };
};

View File

@ -1,7 +1,7 @@
import { QueryData } from 'types/api/widgets/getQuery';
import { SeriesItem } from 'types/api/widgets/getQuery';
const getLabelName = (
metric: QueryData['metric'],
metric: SeriesItem['labels'],
query: string,
legends: string,
): string => {

View File

@ -1,3 +1,4 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GlobalTime } from 'types/actions/globalTime';
import { Widgets } from 'types/api/dashboard/getAll';
@ -6,7 +7,7 @@ const GetMaxMinTime = ({
minTime,
maxTime,
}: GetMaxMinProps): GlobalTime => {
if (graphType === 'VALUE') {
if (graphType === PANEL_TYPES.VALUE) {
return {
maxTime,
minTime: maxTime,

View File

@ -0,0 +1,39 @@
import {
MetricRangePayloadProps,
MetricRangePayloadV3,
} from 'types/api/metrics/getQueryRange';
import { QueryData } from 'types/api/widgets/getQuery';
export const convertNewDataToOld = (
newData: MetricRangePayloadV3,
): MetricRangePayloadProps => {
const { result, resultType } = newData.data;
const oldResult: MetricRangePayloadProps['data']['result'] = [];
result.forEach((item) => {
if (item.series) {
item.series.forEach((serie) => {
const values: QueryData['values'] = serie.values.reduce<
QueryData['values']
>((acc, currentInfo) => {
const renderValues: [number, string] = [
currentInfo.timestamp,
currentInfo.value,
];
return [...acc, renderValues];
}, []);
const result: QueryData = {
metric: serie.labels,
values,
queryName: item.queryName,
};
oldResult.push(result);
});
}
});
const oldResultType = resultType;
return { data: { result: oldResult, resultType: oldResultType } };
};

View File

@ -0,0 +1,53 @@
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
import { QueryBuilderData } from 'types/common/queryBuilder';
type MapQueryDataToApiResult = {
data: Record<string, IBuilderQuery | IBuilderFormula>;
newLegendMap: Record<string, string>;
};
type MapQuery = Record<string, IBuilderQuery>;
type MapFormula = Record<string, IBuilderFormula>;
export const mapQueryDataToApi = (
data: QueryBuilderData,
): MapQueryDataToApiResult => {
const newLegendMap: Record<string, string> = {};
const preparedQueryData: MapQuery = data.queryData.reduce<MapQuery>(
(acc, query) => {
const newResult: MapQuery = {
...acc,
[query.queryName]: {
...query,
},
};
newLegendMap[query.queryName] = query.legend;
return newResult;
},
{},
);
const preparedFormulaData: MapFormula = data.queryFormulas.reduce<MapFormula>(
(acc, formula) => {
const newResult: MapFormula = {
...acc,
[formula.queryName]: {
...formula,
},
};
return newResult;
},
{},
);
return {
data: { ...preparedQueryData, ...preparedFormulaData },
newLegendMap,
};
};

View File

@ -4,19 +4,31 @@ 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);
const value = Array.isArray(item.value) ? item.value.join(', ') : item.value;
return `${item.columnName} ${operator ? operator[1] : ''} ${item.value.join(
' ',
)}`;
return `${item.columnName} ${operator ? operator[1] : ''} ${value}`;
});
return result;
};
export const transformFromStringToHaving = (havingStr: string): Having => {
const [columnName, op, ...value] = havingStr.split(' ');
const [columnName, op, ...values] = havingStr.split(' ');
const operator = Object.entries(OPERATORS).find(([, value]) => value === op);
return { columnName, op: operator ? operator[0] : '', value };
const currentValue = values.reduce<number[]>((acc, strNum) => {
const num = parseFloat(strNum);
if (Number.isNaN(num)) {
return acc;
}
return [...acc, num];
}, []);
return {
columnName,
op: operator ? operator[0] : '',
value: currentValue.length > 1 ? currentValue : currentValue[0],
};
};

View File

@ -19,7 +19,7 @@ import React, {
// TODO: Rename Types on the Reusable type for any source
import {
IBuilderFormula,
IBuilderQueryForm,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
import {
DataSource,
@ -77,8 +77,7 @@ export function QueryBuilderProvider({
const removeEntityByIndex = useCallback(
(type: keyof QueryBuilderData, index: number) => {
setQueryBuilderData((prevState) => {
const currentArray: (IBuilderQueryForm | IBuilderFormula)[] =
prevState[type];
const currentArray: (IBuilderQuery | IBuilderFormula)[] = prevState[type];
return {
...prevState,
[type]: currentArray.filter((item, i) => index !== i),
@ -89,20 +88,20 @@ export function QueryBuilderProvider({
);
const createNewQuery = useCallback(
(queries: IBuilderQueryForm[]): IBuilderQueryForm => {
(queries: IBuilderQuery[]): IBuilderQuery => {
const existNames = queries.map((item) => item.queryName);
const newQuery: IBuilderQueryForm = {
const newQuery: IBuilderQuery = {
...initialQueryBuilderFormValues,
queryName: createNewBuilderItemName({ existNames, sourceNames: alphabet }),
expression: createNewBuilderItemName({
existNames,
sourceNames: alphabet,
}),
...(initialDataSource
? {
dataSource: initialDataSource,
aggregateOperator: mapOfOperators[initialDataSource][0],
expression: createNewBuilderItemName({
existNames,
sourceNames: alphabet,
}),
}
: {}),
};
@ -113,11 +112,14 @@ export function QueryBuilderProvider({
);
const createNewFormula = useCallback((formulas: IBuilderFormula[]) => {
const existNames = formulas.map((item) => item.label);
const existNames = formulas.map((item) => item.queryName);
const newFormula: IBuilderFormula = {
...initialFormulaBuilderFormValues,
label: createNewBuilderItemName({ existNames, sourceNames: formulasNames }),
queryName: createNewBuilderItemName({
existNames,
sourceNames: formulasNames,
}),
};
return newFormula;
@ -153,11 +155,8 @@ export function QueryBuilderProvider({
);
const updateQueryBuilderData = useCallback(
(
queries: IBuilderQueryForm[],
index: number,
newQueryData: IBuilderQueryForm,
) => queries.map((item, idx) => (index === idx ? newQueryData : item)),
(queries: IBuilderQuery[], index: number, newQueryData: IBuilderQuery) =>
queries.map((item, idx) => (index === idx ? newQueryData : item)),
[],
);
@ -168,7 +167,7 @@ export function QueryBuilderProvider({
);
const handleSetQueryData = useCallback(
(index: number, newQueryData: IBuilderQueryForm): void => {
(index: number, newQueryData: IBuilderQuery): void => {
setQueryBuilderData((prevState) => {
const updatedQueryBuilderData = updateQueryBuilderData(
prevState.queryData,

View File

@ -2,8 +2,11 @@ import getDashboard from 'api/dashboard/get';
import {
ClickHouseQueryTemplate,
PromQLQueryTemplate,
QueryBuilderQueryTemplate,
} from 'constants/dashboard';
import {
initialQueryBuilderFormValues,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import GetQueryName from 'lib/query/GetQueryName';
import { Dispatch } from 'redux';
@ -42,7 +45,7 @@ export const GetDashboard = ({
isStacked: false,
nullZeroValues: 'zero',
opacity: '0',
panelTypes: graphType || 'TIME_SERIES',
panelTypes: graphType || PANEL_TYPES.TIME_SERIES,
timePreferance: 'GLOBAL_TIME',
title: '',
queryType: 0,
@ -69,14 +72,9 @@ export const GetDashboard = ({
...ClickHouseQueryTemplate,
},
],
metricsBuilder: {
formulas: [],
queryBuilder: [
{
name: GetQueryName([]) as string,
...QueryBuilderQueryTemplate,
},
],
builder: {
queryFormulas: [],
queryData: [initialQueryBuilderFormValues],
},
},
},

View File

@ -6,7 +6,6 @@ import { getMetricsQueryRange } from 'api/metrics/getQueryRange';
import { AxiosError } from 'axios';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME } from 'container/NewWidget/LeftContainer/QuerySection/constants';
import { EQueryTypeToQueryKeyMapping } from 'container/NewWidget/LeftContainer/QuerySection/types';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
@ -14,15 +13,17 @@ import GetMaxMinTime from 'lib/getMaxMinTime';
import GetMinMax from 'lib/getMinMax';
import GetStartAndEndTime from 'lib/getStartAndEndTime';
import getStep from 'lib/getStep';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
import { Dispatch } from 'redux';
import store from 'store';
import AppActions from 'types/actions';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { IDashboardVariable, Query } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { EDataSource, EPanelType, EQueryType } from 'types/common/dashboard';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { convertNewDataToOld } from 'lib/newQueryBuilder/convertNewDataToOld';
export async function GetMetricQueryRange({
query,
@ -41,51 +42,25 @@ export async function GetMetricQueryRange({
const queryKey: Record<EQueryTypeToQueryKeyMapping, string> =
EQueryTypeToQueryKeyMapping[EQueryType[query.queryType]];
const queryData = query[queryKey];
const legendMap: Record<string, string> = {};
let legendMap: Record<string, string> = {};
const QueryPayload = {
dataSource: EDataSource.METRICS,
compositeMetricQuery: {
queryType,
panelType: EPanelType[graphType],
compositeQuery: {
queryType: queryKey,
panelType: graphType,
},
};
switch (queryType as EQueryType) {
case EQueryType.QUERY_BUILDER: {
const builderQueries = {};
queryData.queryBuilder.map((query) => {
const generatedQueryPayload = {
queryName: query.name,
aggregateOperator: query.aggregateOperator,
metricName: query.metricName,
tagFilters: query.tagFilters,
};
if (graphType === 'TIME_SERIES') {
generatedQueryPayload.groupBy = query.groupBy;
}
// Value
else {
generatedQueryPayload.reduceTo = query.reduceTo;
}
generatedQueryPayload.expression = query.name;
generatedQueryPayload.disabled = query.disabled;
builderQueries[query.name] = generatedQueryPayload;
legendMap[query.name] = query.legend || '';
const { queryData, queryFormulas } = query.builder;
const builderQueries = mapQueryDataToApi({
queryData,
queryFormulas,
});
legendMap = builderQueries.newLegendMap;
queryData[WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME].map((formula) => {
const generatedFormulaPayload = {};
legendMap[formula.name] = formula.legend || formula.name;
generatedFormulaPayload.queryName = formula.name;
generatedFormulaPayload.expression = formula.expression;
generatedFormulaPayload.disabled = formula.disabled;
generatedFormulaPayload.legend = formula.legend;
builderQueries[formula.name] = generatedFormulaPayload;
});
QueryPayload.compositeMetricQuery.builderQueries = builderQueries;
QueryPayload.compositeQuery.builderQueries = builderQueries.data;
break;
}
case EQueryType.CLICKHOUSE: {
@ -98,7 +73,7 @@ export async function GetMetricQueryRange({
};
legendMap[query.name] = query.legend;
});
QueryPayload.compositeMetricQuery.chQueries = chQueries;
QueryPayload.compositeQuery.chQueries = chQueries;
break;
}
case EQueryType.PROM: {
@ -111,7 +86,7 @@ export async function GetMetricQueryRange({
};
legendMap[query.name] = query.legend;
});
QueryPayload.compositeMetricQuery.promQueries = promQueries;
QueryPayload.compositeQuery.promQueries = promQueries;
break;
}
default:
@ -148,7 +123,12 @@ export async function GetMetricQueryRange({
`API responded with ${response.statusCode} - ${response.error}`,
);
}
if (response.payload?.data?.result) {
const v2Range = convertNewDataToOld(response.payload);
response.payload = v2Range;
response.payload.data.result = response.payload.data.result.map(
(queryData) => {
const newQueryData = queryData;
@ -164,6 +144,7 @@ export async function GetMetricQueryRange({
newQueryData.metric[queryData.queryName] = queryData.queryName;
}
}
return newQueryData;
},
);
@ -182,6 +163,7 @@ export const GetQueryResults = (
errorMessage: '',
widgetId: props.widgetId,
errorBoolean: false,
isLoadingQueryResult: true,
},
});
const response = await GetMetricQueryRange(props);
@ -194,6 +176,7 @@ export const GetQueryResults = (
payload: {
errorMessage: isError || '',
widgetId: props.widgetId,
isLoadingQueryResult: false,
},
});
return;
@ -217,6 +200,7 @@ export const GetQueryResults = (
errorMessage: (error as AxiosError).toString(),
widgetId: props.widgetId,
errorBoolean: true,
isLoadingQueryResult: false,
},
});
}

View File

@ -32,6 +32,7 @@ const InitialValue: InitialValueTypes = {
isEditMode: false,
isQueryFired: false,
isAddWidget: false,
isLoadingQueryResult: false,
};
const dashboard = (
@ -170,7 +171,12 @@ const dashboard = (
}
case QUERY_ERROR: {
const { widgetId, errorMessage, errorBoolean = true } = action.payload;
const {
widgetId,
errorMessage,
errorBoolean = true,
isLoadingQueryResult = false,
} = action.payload;
const [selectedDashboard] = state.dashboards;
const { data } = selectedDashboard;
@ -210,6 +216,7 @@ const dashboard = (
},
],
isQueryFired: true,
isLoadingQueryResult,
};
}
@ -255,6 +262,7 @@ const dashboard = (
},
],
isQueryFired: true,
isLoadingQueryResult: false,
};
}

View File

@ -152,6 +152,7 @@ interface QueryError {
errorMessage: string;
widgetId: string;
errorBoolean?: boolean;
isLoadingQueryResult?: boolean;
};
}

View File

@ -6,6 +6,7 @@ import {
EQueryType,
EReduceOperator,
} from 'types/common/dashboard';
import { QueryBuilderData } from 'types/common/queryBuilder';
import { QueryData } from '../widgets/getQuery';
@ -92,10 +93,7 @@ export interface PromQLWidgets extends IBaseWidget {
export interface Query {
queryType: EQueryType;
promQL: IPromQLQuery[];
metricsBuilder: {
formulas: IMetricsBuilderFormula[];
queryBuilder: IMetricsBuilderQuery[];
};
builder: QueryBuilderData;
clickHouse: IClickHouseQuery[];
}

View File

@ -1,10 +1,16 @@
import { QueryData } from '../widgets/getQuery';
import { QueryData, QueryDataV3 } from '../widgets/getQuery';
export type MetricsRangeProps = never;
export interface MetricRangePayloadProps {
data: {
result: QueryData[];
resultType: string;
variables: Record<string, unknown>;
};
}
export interface MetricRangePayloadV3 {
data: {
result: QueryDataV3[];
resultType: string;
};
}

View File

@ -1,11 +1,8 @@
import { DataSource } from 'types/common/queryBuilder';
import { BaseAutocompleteData } from './queryAutocompleteResponse';
export interface IGetAttributeKeysPayload {
aggregateOperator: string;
dataSource: DataSource;
searchText: string;
aggregateAttribute: string;
tagType: BaseAutocompleteData['type'];
}

View File

@ -1,4 +1,4 @@
import { DataSource } from 'types/common/queryBuilder';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { BaseAutocompleteData } from './queryAutocompleteResponse';
@ -6,7 +6,8 @@ import { BaseAutocompleteData } from './queryAutocompleteResponse';
export interface IBuilderFormula {
expression: string;
disabled: boolean;
label: string;
queryName: string;
dataSource?: DataSource;
legend: string;
}
@ -27,6 +28,10 @@ export interface TagFilter {
export type Having = {
columnName: string;
op: string;
value: number | number[];
};
export type HavingForm = Omit<Having, 'value'> & {
value: string[];
};
@ -35,7 +40,7 @@ export type IBuilderQuery = {
queryName: string;
dataSource: DataSource;
aggregateOperator: string;
aggregateAttribute: string;
aggregateAttribute: BaseAutocompleteData;
tagFilters: TagFilter;
groupBy: BaseAutocompleteData[];
expression: string;
@ -44,10 +49,6 @@ export type IBuilderQuery = {
limit: number | null;
stepInterval: number;
orderBy: BaseAutocompleteData[];
reduceTo: string;
};
export type IBuilderQueryForm = Omit<IBuilderQuery, 'aggregateAttribute'> & {
aggregateAttribute: BaseAutocompleteData;
reduceTo: ReduceOperators;
legend: string;
};

View File

@ -4,8 +4,7 @@ export interface PayloadProps {
}
export interface QueryData {
metric?: {
__name__: string;
metric: {
[key: string]: string;
};
queryName: string;
@ -13,6 +12,20 @@ export interface QueryData {
values: [number, string][];
}
export interface SeriesItem {
labels: {
[key: string]: string;
};
values: { timestamp: number; value: string }[];
}
export interface QueryDataV3 {
list: null;
queryName: string;
legend?: string;
series: SeriesItem[];
}
export interface Props {
query: string;
step: string;

View File

@ -1,9 +1,3 @@
export enum EDataSource {
METRICS = 1,
TRACES,
LOGS,
}
export enum EQueryType {
QUERY_BUILDER = 1,
CLICKHOUSE,
@ -42,8 +36,8 @@ export enum EAggregateOperator {
}
export enum EPanelType {
TIME_SERIES = 1,
VALUE,
GRAPH = 'graph',
VALUE = 'value',
}
export enum EReduceOperator {

View File

@ -0,0 +1,30 @@
import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
type UseQueryOperationsParams = Pick<
QueryProps,
'index' | 'panelType' | 'query'
>;
export type HandleChangeQueryData = <
Key extends keyof IBuilderQuery,
Value extends IBuilderQuery[Key]
>(
key: Key,
value: Value,
) => void;
export type UseQueryOperations = (
params: UseQueryOperationsParams,
) => {
isMetricsDataSource: boolean;
operators: string[];
listOfAdditionalFilters: string[];
handleChangeOperator: (value: string) => void;
handleChangeAggregatorAttribute: (value: BaseAutocompleteData) => void;
handleChangeDataSource: (newSource: DataSource) => void;
handleDeleteQuery: () => void;
handleChangeQueryData: HandleChangeQueryData;
};

View File

@ -1,6 +1,6 @@
import {
IBuilderFormula,
IBuilderQueryForm,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
export enum DataSource {
@ -103,6 +103,10 @@ export enum TracesAggregatorOperator {
P95 = 'p95',
P99 = 'p99',
RATE = 'rate',
RATE_SUM = 'rate_sum',
RATE_AVG = 'rate_avg',
RATE_MIN = 'rate_min',
RATE_MAX = 'rate_max',
}
export enum LogsAggregatorOperator {
@ -123,18 +127,23 @@ export enum LogsAggregatorOperator {
P95 = 'p95',
P99 = 'p99',
RATE = 'rate',
RATE_SUM = 'rate_sum',
RATE_AVG = 'rate_avg',
RATE_MIN = 'rate_min',
RATE_MAX = 'rate_max',
}
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 PanelTypeKeys =
| 'TIME_SERIES'
| 'VALUE'
| 'TABLE'
| 'LIST'
| 'EMPTY_WIDGET';
export type ReduceOperators = 'last' | 'sum' | 'avg' | 'max' | 'min';
export type QueryBuilderData = {
queryData: IBuilderQueryForm[];
queryData: IBuilderQuery[];
queryFormulas: IBuilderFormula[];
};
@ -143,7 +152,7 @@ export type QueryBuilderContextType = {
queryBuilderData: QueryBuilderData;
initialDataSource: DataSource | null;
resetQueryBuilderData: () => void;
handleSetQueryData: (index: number, queryData: IBuilderQueryForm) => void;
handleSetQueryData: (index: number, queryData: IBuilderQuery) => void;
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
initQueryBuilderData: (queryBuilderData: QueryBuilderData) => void;
setupInitialDataSource: (newInitialDataSource: DataSource | null) => void;
@ -151,3 +160,8 @@ export type QueryBuilderContextType = {
addNewQuery: () => void;
addNewFormula: () => void;
};
export type QueryAdditionalFilter = {
field: keyof IBuilderQuery;
text: string;
};

View File

@ -8,4 +8,5 @@ export default interface DashboardReducer {
isEditMode: boolean;
isQueryFired: boolean;
isAddWidget: boolean;
isLoadingQueryResult: boolean;
}