mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 04:19:03 +08:00
feat(filter): add group by filter (#2538)
This commit is contained in:
parent
d4bfe3a096
commit
5f73a82d9f
@ -11,6 +11,7 @@ export const getAggregateKeys = async ({
|
|||||||
searchText,
|
searchText,
|
||||||
dataSource,
|
dataSource,
|
||||||
aggregateAttribute,
|
aggregateAttribute,
|
||||||
|
tagType,
|
||||||
}: IGetAttributeKeysPayload): Promise<
|
}: IGetAttributeKeysPayload): Promise<
|
||||||
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||||
> => {
|
> => {
|
||||||
@ -18,7 +19,7 @@ export const getAggregateKeys = async ({
|
|||||||
const response: AxiosResponse<{
|
const response: AxiosResponse<{
|
||||||
data: IQueryAutocompleteResponse;
|
data: IQueryAutocompleteResponse;
|
||||||
}> = await ApiV3Instance.get(
|
}> = await ApiV3Instance.get(
|
||||||
`autocomplete/attribute_keys?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&aggregateAttribute=${aggregateAttribute}&searchText=${searchText}`,
|
`autocomplete/attribute_keys?aggregateOperator=${aggregateOperator}&dataSource=${dataSource}&aggregateAttribute=${aggregateAttribute}&tagType=${tagType}&searchText=${searchText}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export enum QueryBuilderKeys {
|
export enum QueryBuilderKeys {
|
||||||
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
||||||
|
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
export const StyledLabel = styled.div`
|
export const StyledLabel = styled.div`
|
||||||
padding: 0 0.6875rem;
|
padding: 0 0.6875rem;
|
||||||
min-width: 6.5rem;
|
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const StyledButton = styled(Button)`
|
export const StyledButton = styled(Button)<{ isAvailableToDisable: boolean }>`
|
||||||
min-width: 2rem;
|
min-width: 2rem;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
padding: 0.125rem;
|
padding: 0.125rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
|
margin-right: 0.1rem;
|
||||||
|
pointer-events: ${(props): string =>
|
||||||
|
props.isAvailableToDisable ? 'default' : 'none'};
|
||||||
`;
|
`;
|
||||||
|
@ -30,7 +30,8 @@ export function ListMarker({
|
|||||||
icon={buttonProps.icon}
|
icon={buttonProps.icon}
|
||||||
onClick={buttonProps.onClick}
|
onClick={buttonProps.onClick}
|
||||||
className={className}
|
className={className}
|
||||||
style={{ marginRight: '0.1rem', ...style }}
|
isAvailableToDisable={isAvailableToDisable}
|
||||||
|
style={style}
|
||||||
>
|
>
|
||||||
{labelName}
|
{labelName}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
|
@ -8,13 +8,14 @@ import {
|
|||||||
} from 'container/QueryBuilder/components';
|
} from 'container/QueryBuilder/components';
|
||||||
import {
|
import {
|
||||||
AggregatorFilter,
|
AggregatorFilter,
|
||||||
|
GroupByFilter,
|
||||||
OperatorsSelect,
|
OperatorsSelect,
|
||||||
} from 'container/QueryBuilder/filters';
|
} from 'container/QueryBuilder/filters';
|
||||||
// Context
|
// Context
|
||||||
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/useQueryBuilder';
|
||||||
// ** Hooks
|
// ** Hooks
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
// ** Constants
|
// ** Constants
|
||||||
import {
|
import {
|
||||||
@ -55,13 +56,21 @@ export function Query({
|
|||||||
handleSetQueryData(index, { disabled: !query.disabled });
|
handleSetQueryData(index, { disabled: !query.disabled });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeAggregatorAttribute = (value: AutocompleteData): void => {
|
const handleChangeAggregatorAttribute = (
|
||||||
|
value: BaseAutocompleteData,
|
||||||
|
): void => {
|
||||||
handleSetQueryData(index, { aggregateAttribute: value });
|
handleSetQueryData(index, { aggregateAttribute: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeGroupByKeys = (values: BaseAutocompleteData[]): void => {
|
||||||
|
handleSetQueryData(index, { groupBy: values });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row style={{ gap: '0.75rem' }}>
|
<Row gutter={[0, 15]}>
|
||||||
<Col span={12}>
|
<Col span={24}>
|
||||||
|
<Row wrap={false} align="middle">
|
||||||
|
<Col span={24}>
|
||||||
<ListMarker
|
<ListMarker
|
||||||
isDisabled={query.disabled}
|
isDisabled={query.disabled}
|
||||||
toggleDisabled={handleToggleDisableQuery}
|
toggleDisabled={handleToggleDisableQuery}
|
||||||
@ -77,19 +86,37 @@ export function Query({
|
|||||||
) : (
|
) : (
|
||||||
<FilterLabel label={transformToUpperCase(query.dataSource)} />
|
<FilterLabel label={transformToUpperCase(query.dataSource)} />
|
||||||
)}
|
)}
|
||||||
|
{/* TODO: here will be search */}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<Col span={11}>
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="95px">
|
||||||
<OperatorsSelect
|
<OperatorsSelect
|
||||||
value={query.aggregateOperator || currentListOfOperators[0]}
|
value={query.aggregateOperator || currentListOfOperators[0]}
|
||||||
onChange={handleChangeOperator}
|
onChange={handleChangeOperator}
|
||||||
operators={currentListOfOperators}
|
operators={currentListOfOperators}
|
||||||
style={{ minWidth: 104, marginRight: '0.75rem' }}
|
|
||||||
/>
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 200px">
|
||||||
<AggregatorFilter
|
<AggregatorFilter
|
||||||
onChange={handleChangeAggregatorAttribute}
|
onChange={handleChangeAggregatorAttribute}
|
||||||
query={query}
|
query={query}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<Col span={11} offset={2}>
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="95px">
|
||||||
|
<FilterLabel label="Group by" />
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 200px">
|
||||||
|
<GroupByFilter query={query} onChange={handleChangeGroupByKeys} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { AutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
export type AgregatorFilterProps = {
|
export type AgregatorFilterProps = {
|
||||||
onChange: (value: AutocompleteData) => void;
|
onChange: (value: BaseAutocompleteData) => void;
|
||||||
query: IBuilderQueryForm;
|
query: IBuilderQueryForm;
|
||||||
};
|
};
|
||||||
|
@ -68,10 +68,8 @@ export function AggregatorFilter({
|
|||||||
return (
|
return (
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
showSearch
|
showSearch
|
||||||
placeholder={`${transformToUpperCase(
|
placeholder={`${transformToUpperCase(query.dataSource)} name`}
|
||||||
query.dataSource,
|
style={{ width: '100%' }}
|
||||||
)} Name (Start typing to get suggestions)`}
|
|
||||||
style={{ flex: 1, minWidth: 200 }}
|
|
||||||
showArrow={false}
|
showArrow={false}
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
onSearch={handleSearchAttribute}
|
onSearch={handleSearchAttribute}
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export type GroupByFilterProps = {
|
||||||
|
query: IBuilderQueryForm;
|
||||||
|
onChange: (values: BaseAutocompleteData[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GroupByFilterValue = {
|
||||||
|
disabled: boolean | undefined;
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
title: string | undefined;
|
||||||
|
value: string;
|
||||||
|
};
|
@ -0,0 +1,100 @@
|
|||||||
|
import { Select, Spin } from 'antd';
|
||||||
|
// ** Api
|
||||||
|
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||||
|
// ** Constants
|
||||||
|
import { QueryBuilderKeys } from 'constants/useQueryKeys';
|
||||||
|
// ** Components
|
||||||
|
// ** Helpers
|
||||||
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { SelectOption } from 'types/common/select';
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import {
|
||||||
|
GroupByFilterProps,
|
||||||
|
GroupByFilterValue,
|
||||||
|
} from './GroupByFilter.interfaces';
|
||||||
|
|
||||||
|
export function GroupByFilter({
|
||||||
|
query,
|
||||||
|
onChange,
|
||||||
|
}: GroupByFilterProps): JSX.Element {
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
|
||||||
|
const { data, isFetching } = useQuery(
|
||||||
|
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
|
||||||
|
async () =>
|
||||||
|
getAggregateKeys({
|
||||||
|
aggregateAttribute: query.aggregateAttribute.key,
|
||||||
|
tagType: query.aggregateAttribute.type,
|
||||||
|
dataSource: query.dataSource,
|
||||||
|
aggregateOperator: query.aggregateOperator,
|
||||||
|
searchText,
|
||||||
|
}),
|
||||||
|
{ enabled: !!query.aggregateAttribute.key, keepPreviousData: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearchKeys = (searchText: string): void => {
|
||||||
|
setSearchText(searchText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionsData: SelectOption<string, string>[] =
|
||||||
|
data?.payload?.attributeKeys?.map((item) => ({
|
||||||
|
label: transformStringWithPrefix({
|
||||||
|
str: item.key,
|
||||||
|
prefix: item.type || '',
|
||||||
|
condition: !item.isColumn,
|
||||||
|
}),
|
||||||
|
value: item.key,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isColumn: null,
|
||||||
|
key: item.value,
|
||||||
|
dataType: null,
|
||||||
|
type: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onChange(groupByValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const values: GroupByFilterValue[] = query.groupBy.map((item) => ({
|
||||||
|
label: transformStringWithPrefix({
|
||||||
|
str: item.key,
|
||||||
|
prefix: item.type || '',
|
||||||
|
condition: !item.isColumn,
|
||||||
|
}),
|
||||||
|
key: item.key,
|
||||||
|
value: item.key,
|
||||||
|
disabled: undefined,
|
||||||
|
title: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onSearch={handleSearchKeys}
|
||||||
|
showSearch
|
||||||
|
disabled={!query.aggregateAttribute.key}
|
||||||
|
showArrow={false}
|
||||||
|
filterOption={false}
|
||||||
|
options={optionsData}
|
||||||
|
labelInValue
|
||||||
|
value={values}
|
||||||
|
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { GroupByFilter } from './GroupByFilter';
|
@ -25,6 +25,7 @@ export function OperatorsSelect({
|
|||||||
options={operatorsOptions}
|
options={operatorsOptions}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
style={{ width: '100%' }}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export { AggregatorFilter } from './AggregatorFilter';
|
export { AggregatorFilter } from './AggregatorFilter';
|
||||||
|
export { GroupByFilter } from './GroupByFilter';
|
||||||
export { OperatorsSelect } from './OperatorsSelect';
|
export { OperatorsSelect } from './OperatorsSelect';
|
||||||
|
@ -53,6 +53,7 @@ export function QueryBuilderProvider({
|
|||||||
isColumn: null,
|
isColumn: null,
|
||||||
type: null,
|
type: null,
|
||||||
},
|
},
|
||||||
|
groupBy: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
queryFormulas: [],
|
queryFormulas: [],
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { BaseAutocompleteData } from './queryAutocompleteResponse';
|
||||||
|
|
||||||
export interface IGetAttributeKeysPayload {
|
export interface IGetAttributeKeysPayload {
|
||||||
aggregateOperator: string;
|
aggregateOperator: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
aggregateAttribute: string;
|
aggregateAttribute: string;
|
||||||
|
tagType: BaseAutocompleteData['type'];
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export interface AutocompleteData {
|
export interface BaseAutocompleteData {
|
||||||
dataType: 'number' | 'string' | 'boolean' | null;
|
dataType: 'number' | 'string' | 'boolean' | null;
|
||||||
isColumn: boolean | null;
|
isColumn: boolean | null;
|
||||||
key: string;
|
key: string;
|
||||||
@ -6,5 +6,5 @@ export interface AutocompleteData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IQueryAutocompleteResponse {
|
export interface IQueryAutocompleteResponse {
|
||||||
attributeKeys: AutocompleteData[];
|
attributeKeys: BaseAutocompleteData[];
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { AutocompleteData } from './queryAutocompleteResponse';
|
import { BaseAutocompleteData } from './queryAutocompleteResponse';
|
||||||
|
|
||||||
// Type for Formula
|
// Type for Formula
|
||||||
export interface IBuilderFormula {
|
export interface IBuilderFormula {
|
||||||
@ -30,7 +30,7 @@ export type IBuilderQuery = {
|
|||||||
aggregateOperator: string;
|
aggregateOperator: string;
|
||||||
aggregateAttribute: string;
|
aggregateAttribute: string;
|
||||||
tagFilters: TagFilter[];
|
tagFilters: TagFilter[];
|
||||||
groupBy: string[];
|
groupBy: BaseAutocompleteData[];
|
||||||
expression: string;
|
expression: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
having?: string;
|
having?: string;
|
||||||
@ -40,5 +40,5 @@ export type IBuilderQuery = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type IBuilderQueryForm = Omit<IBuilderQuery, 'aggregateAttribute'> & {
|
export type IBuilderQueryForm = Omit<IBuilderQuery, 'aggregateAttribute'> & {
|
||||||
aggregateAttribute: AutocompleteData;
|
aggregateAttribute: BaseAutocompleteData;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user