feat: added Order By filter (#2551)

* fix: remove frontend code owner

* chore: set Cache-Control for auto complete requests (#2504)

* feat(filter): add group by filter (#2538)

* feat: poc of search bar

* feat: poc of search bar

* feat: attribute keys auto complete  api

* chore: conflict resolve

* chore: conflict resolve

* fix: menu was not open on click

* feat: re-used antoney's hooks code

* fix: linting & type issue

* fix: unwanted file changes

* fix: conflic changes

* feat: added orderby filter

* chore: rebased changes

* feat: poc of search bar

* feat: poc of search bar

* feat: attribute keys auto complete  api

* chore: conflict resolve

* fix: menu was not open on click

* feat: re-used antoney's hooks code

* fix: linting & type issue

* fix: uncomment qb component

* fix: unwanted file changes

* fix: conflic changes

* fix: suggested changes

* fix: reused label component

* fix: unwanted changes

* fix: unwanted changes

* fix: recovered old changes

* fix: orderby reset behaviour

* chore: rebased changes

* fix: resolved unwanted changes

* fix: ui of filter row

* fix: resolved order by filter issue on label

* fix: resolved reset behaviour

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
Co-authored-by: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
Chintan Sudani 2023-04-18 17:17:06 +05:30 committed by GitHub
parent 60b78e94d8
commit 63570c847a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 294 additions and 39 deletions

View File

@ -44,7 +44,7 @@ export const initialQueryBuilderFormValues: IBuilderQueryForm = {
queryName: createNewQueryName([]), queryName: createNewQueryName([]),
aggregateOperator: Object.values(MetricAggregateOperator)[0], aggregateOperator: Object.values(MetricAggregateOperator)[0],
aggregateAttribute: initialAggregateAttribute, aggregateAttribute: initialAggregateAttribute,
tagFilters: [], tagFilters: { items: [], op: 'AND' },
expression: '', expression: '',
disabled: false, disabled: false,
having: [], having: [],

View File

@ -1,4 +1,3 @@
import { Row } from 'antd';
import React, { Fragment, memo, ReactNode, useState } from 'react'; import React, { Fragment, memo, ReactNode, useState } from 'react';
// ** Types // ** Types
@ -45,7 +44,7 @@ export const AdditionalFiltersToggler = memo(function AdditionalFiltersToggler({
{isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />} {isOpenedFilters ? <StyledIconClose /> : <StyledIconOpen />}
{!isOpenedFilters && <span>Add conditions for {filtersTexts}</span>} {!isOpenedFilters && <span>Add conditions for {filtersTexts}</span>}
</StyledInner> </StyledInner>
{isOpenedFilters && <Row>{children}</Row>} {isOpenedFilters && children}
</div> </div>
); );
}); });

View File

@ -20,3 +20,7 @@ export const StyledDeleteEntity = styled(CloseCircleOutlined)`
export const StyledRow = styled(Row)` export const StyledRow = styled(Row)`
padding-right: 3rem; padding-right: 3rem;
`; `;
export const StyledFilterRow = styled(Row)`
margin-bottom: 0.875rem;
`;

View File

@ -21,20 +21,24 @@ import {
} from 'container/QueryBuilder/filters'; } from 'container/QueryBuilder/filters';
import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryFilter'; import AggregateEveryFilter from 'container/QueryBuilder/filters/AggregateEveryFilter';
import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter'; import LimitFilter from 'container/QueryBuilder/filters/LimitFilter/LimitFilter';
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useQueryBuilder } from 'hooks/useQueryBuilder'; import { useQueryBuilder } from 'hooks/useQueryBuilder';
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator'; import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
// ** Hooks // ** Hooks
import React, { memo, useCallback, useMemo } from 'react'; import React, { memo, useCallback, useMemo } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; import {
IBuilderQueryForm,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { transformToUpperCase } from 'utils/transformToUpperCase'; import { transformToUpperCase } from 'utils/transformToUpperCase';
// ** Types // ** Types
import { QueryProps } from './Query.interfaces'; import { QueryProps } from './Query.interfaces';
// ** Styles // ** Styles
import { StyledDeleteEntity, StyledRow } from './Query.styled'; import { StyledDeleteEntity, StyledFilterRow, StyledRow } from './Query.styled';
export const Query = memo(function Query({ export const Query = memo(function Query({
index, index,
@ -66,10 +70,13 @@ export const Query = memo(function Query({
...query, ...query,
aggregateOperator: value, aggregateOperator: value,
having: [], having: [],
groupBy: [],
orderBy: [],
limit: null, limit: null,
tagFilters: { items: [], op: 'AND' },
}; };
if (!aggregateDataType || query.dataSource === DataSource.METRICS) { if (!aggregateDataType) {
handleSetQueryData(index, newQuery); handleSetQueryData(index, newQuery);
return; return;
} }
@ -194,6 +201,17 @@ export const Query = memo(function Query({
[query.dataSource], [query.dataSource],
); );
const handleChangeOrderByKeys = useCallback(
(values: BaseAutocompleteData[]): void => {
const newQuery: IBuilderQueryForm = {
...query,
orderBy: values,
};
handleSetQueryData(index, newQuery);
},
[handleSetQueryData, index, query],
);
const handleChangeLimit = useCallback( const handleChangeLimit = useCallback(
(value: number | null): void => { (value: number | null): void => {
const newQuery: IBuilderQueryForm = { const newQuery: IBuilderQueryForm = {
@ -216,6 +234,17 @@ export const Query = memo(function Query({
[index, query, handleSetQueryData], [index, query, handleSetQueryData],
); );
const handleChangeTagFilters = useCallback(
(value: TagFilter): void => {
const newQuery: IBuilderQueryForm = {
...query,
tagFilters: value,
};
handleSetQueryData(index, newQuery);
},
[index, query, handleSetQueryData],
);
return ( return (
<StyledRow gutter={[0, 15]}> <StyledRow gutter={[0, 15]}>
<StyledDeleteEntity onClick={handleDeleteQuery} /> <StyledDeleteEntity onClick={handleDeleteQuery} />
@ -241,7 +270,7 @@ export const Query = memo(function Query({
{isMatricsDataSource && <FilterLabel label="WHERE" />} {isMatricsDataSource && <FilterLabel label="WHERE" />}
</Col> </Col>
<Col span={isMatricsDataSource ? 17 : 20}> <Col span={isMatricsDataSource ? 17 : 20}>
<QueryBuilderSearch query={query} /> <QueryBuilderSearch query={query} onChange={handleChangeTagFilters} />
</Col> </Col>
</Row> </Row>
</Col> </Col>
@ -279,20 +308,26 @@ export const Query = memo(function Query({
<Col span={24}> <Col span={24}>
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}> <AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
{!isMatricsDataSource && ( {!isMatricsDataSource && (
<Row gutter={[11, 5]}> <StyledFilterRow gutter={[11, 5]} justify="space-around">
<Col span={6}> <Col span={2}>
<FilterLabel label="Order by" />
</Col>
<Col span={10}>
<OrderByFilter query={query} onChange={handleChangeOrderByKeys} />
</Col>
<Col span={1.5}>
<FilterLabel label="Limit" /> <FilterLabel label="Limit" />
</Col> </Col>
<Col span={18}> <Col span={10}>
<LimitFilter query={query} onChange={handleChangeLimit} /> <LimitFilter query={query} onChange={handleChangeLimit} />
</Col> </Col>
</Row> </StyledFilterRow>
)} )}
<Row gutter={[11, 5]}> <Row gutter={[11, 5]}>
<Col span={10}> <Col span={3}>
<FilterLabel label="Aggregate Every" /> <FilterLabel label="Aggregate Every" />
</Col> </Col>
<Col span={14}> <Col span={8}>
<AggregateEveryFilter <AggregateEveryFilter
query={query} query={query}
onChange={handleChangeAggregateEvery} onChange={handleChangeAggregateEvery}

View File

@ -1,17 +1,17 @@
import { Select, Spin } from 'antd'; import { Select, Spin } from 'antd';
// ** Api
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys'; import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
// ** Constants // ** Constants
import { QueryBuilderKeys } from 'constants/queryBuilder'; import { QueryBuilderKeys } from 'constants/queryBuilder';
// ** Components // ** Components
// ** Helpers // ** Helpers
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import React, { memo, useState } from 'react'; import React, { memo, useMemo, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { MetricAggregateOperator } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select'; import { SelectOption } from 'types/common/select';
// ** Types import { selectStyle } from '../QueryBuilderSearch/config';
import { import {
GroupByFilterProps, GroupByFilterProps,
GroupByFilterValue, GroupByFilterValue,
@ -81,13 +81,20 @@ export const GroupByFilter = memo(function GroupByFilter({
title: undefined, title: undefined,
})); }));
const isDisabledSelect = useMemo(
() =>
!query.aggregateAttribute.key ||
query.aggregateOperator === MetricAggregateOperator.NOOP,
[query.aggregateAttribute.key, query.aggregateOperator],
);
return ( return (
<Select <Select
mode="tags" mode="tags"
style={{ width: '100%' }} style={selectStyle}
onSearch={handleSearchKeys} onSearch={handleSearchKeys}
showSearch showSearch
disabled={!query.aggregateAttribute.key} disabled={isDisabledSelect}
showArrow={false} showArrow={false}
filterOption={false} filterOption={false}
options={optionsData} options={optionsData}

View File

@ -1,30 +1,21 @@
import { InputNumber } from 'antd'; import { InputNumber } from 'antd';
import React, { useState } from 'react'; import React from 'react';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { selectStyle } from '../QueryBuilderSearch/config'; import { selectStyle } from '../QueryBuilderSearch/config';
function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element { function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
const [isData, setIsData] = useState<number | null>(null);
const onChangeHandler = (value: number | null): void => { const onChangeHandler = (value: number | null): void => {
setIsData(value); onChange(value);
};
const handleEnter = (e: { key: string }): void => {
if (e.key === 'Enter') {
onChange(isData);
}
}; };
return ( return (
<InputNumber <InputNumber
min={1} min={1}
type="number" type="number"
placeholder="e.g 10"
disabled={!query.aggregateAttribute.key} disabled={!query.aggregateAttribute.key}
style={selectStyle} style={selectStyle}
onChange={onChangeHandler} onChange={onChangeHandler}
onPressEnter={handleEnter}
/> />
); );
} }

View File

@ -5,6 +5,7 @@ import { SelectOption } from 'types/common/select';
// ** Helpers // ** Helpers
import { transformToUpperCase } from 'utils/transformToUpperCase'; import { transformToUpperCase } from 'utils/transformToUpperCase';
import { selectStyle } from '../QueryBuilderSearch/config';
import { OperatorsSelectProps } from './OperatorsSelect.interfaces'; import { OperatorsSelectProps } from './OperatorsSelect.interfaces';
export const OperatorsSelect = memo(function OperatorsSelect({ export const OperatorsSelect = memo(function OperatorsSelect({
@ -25,7 +26,7 @@ export const OperatorsSelect = memo(function OperatorsSelect({
options={operatorsOptions} options={operatorsOptions}
value={value} value={value}
onChange={onChange} onChange={onChange}
style={{ width: '100%' }} style={selectStyle}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...props} {...props}
/> />

View File

@ -0,0 +1,15 @@
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
export type OrderByFilterProps = {
query: IBuilderQueryForm;
onChange: (values: BaseAutocompleteData[]) => void;
};
export type OrderByFilterValue = {
disabled: boolean | undefined;
key: string;
label: string;
title: string | undefined;
value: string;
};

View File

@ -0,0 +1,141 @@
import { Select, Spin } from 'antd';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { IOption } from 'hooks/useResourceAttribute/types';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import React, { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregateOperator } from 'types/common/queryBuilder';
import { selectStyle } from '../QueryBuilderSearch/config';
import {
OrderByFilterProps,
OrderByFilterValue,
} from './OrderByFilter.interfaces';
import { getLabelFromValue, mapLabelValuePairs } from './utils';
export function OrderByFilter({
query,
onChange,
}: OrderByFilterProps): JSX.Element {
const [searchText, setSearchText] = useState<string>('');
const [selectedValue, setSelectedValue] = useState<OrderByFilterValue[]>([]);
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 = useCallback(
(searchText: string): void => setSearchText(searchText),
[],
);
const generateOptionsData = (
attributeKeys: BaseAutocompleteData[] | undefined,
selectedValue: OrderByFilterValue[],
query: IBuilderQueryForm,
): IOption[] => {
const selectedValueLabels = getLabelFromValue(selectedValue);
const noAggregationOptions = attributeKeys
? mapLabelValuePairs(attributeKeys)
.flat()
.filter(
(option) => !selectedValueLabels.includes(option.label.split(' ')[0]),
)
: [];
const aggregationOptions = mapLabelValuePairs(query.groupBy)
.flat()
.concat([
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
},
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
},
])
.filter(
(option) => !selectedValueLabels.includes(option.label.split(' ')[0]),
);
return query.aggregateOperator === MetricAggregateOperator.NOOP
? noAggregationOptions
: aggregationOptions;
};
const optionsData = useMemo(
() => generateOptionsData(data?.payload?.attributeKeys, selectedValue, query),
[data?.payload?.attributeKeys, query, selectedValue],
);
const handleChange = (values: OrderByFilterValue[]): void => {
setSelectedValue(values);
const orderByValues: BaseAutocompleteData[] = values?.map((item) => {
const iterationArray = data?.payload?.attributeKeys || query.orderBy;
const existingOrderValues = iterationArray.find(
(group) => group.key === item.value,
);
if (existingOrderValues) {
return existingOrderValues;
}
return {
isColumn: null,
key: item.value,
dataType: null,
type: null,
};
});
onChange(orderByValues);
};
const values: OrderByFilterValue[] = query.orderBy.map((item) => ({
label: transformStringWithPrefix({
str: item.key,
prefix: item.type || '',
condition: !item.isColumn,
}),
key: item.key,
value: item.key,
disabled: undefined,
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}
showArrow={false}
filterOption={false}
options={optionsData}
labelInValue
value={values}
notFoundContent={isFetching ? <Spin size="small" /> : null}
onChange={handleChange}
/>
);
}

View File

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

View File

@ -0,0 +1,32 @@
import { IOption } from 'hooks/useResourceAttribute/types';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OrderByFilterValue } from './OrderByFilter.interfaces';
export function mapLabelValuePairs(
arr: BaseAutocompleteData[],
): Array<IOption>[] {
return arr.map((item) => {
const label = transformStringWithPrefix({
str: item.key,
prefix: item.type || '',
condition: !item.isColumn,
});
const value = item.key;
return [
{
label: `${label} asc`,
value: `${value} asc`,
},
{
label: `${label} desc`,
value: `${value} desc`,
},
];
});
}
export function getLabelFromValue(arr: OrderByFilterValue[]): string[] {
return arr.map((value) => value.label.split(' ')[0]);
}

View File

@ -1,13 +1,20 @@
import { Select, Spin, Tag, Tooltip } from 'antd'; import { Select, Spin, Tag, Tooltip } from 'antd';
import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete'; import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete';
import React from 'react'; import React, { useEffect, useMemo } from 'react';
import { IBuilderQueryForm } from 'types/api/queryBuilder/queryBuilderData'; import {
IBuilderQueryForm,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { selectStyle } from './config'; import { selectStyle } from './config';
import { StyledCheckOutlined, TypographyText } from './style'; import { StyledCheckOutlined, TypographyText } from './style';
import { isInNotInOperator } from './utils'; import { isInNotInOperator } from './utils';
function QueryBuilderSearch({ query }: QueryBuilderSearchProps): JSX.Element { function QueryBuilderSearch({
query,
onChange,
}: QueryBuilderSearchProps): JSX.Element {
const { const {
handleClearTag, handleClearTag,
handleKeyDown, handleKeyDown,
@ -51,6 +58,26 @@ function QueryBuilderSearch({ query }: QueryBuilderSearchProps): JSX.Element {
if (isMulti || event.key === 'Backspace') handleKeyDown(event); if (isMulti || event.key === 'Backspace') handleKeyDown(event);
}; };
const queryTags = useMemo(() => {
if (!query.aggregateAttribute.key) return [];
return tags;
}, [query.aggregateAttribute.key, tags]);
useEffect(() => {
const initialTagFilters: TagFilter = { items: [], op: 'AND' };
initialTagFilters.items = tags.map((tag) => {
const [tagKey, tagOperator, ...tagValue] = tag.split(' ');
return {
id: uuid().slice(0, 8),
key: tagKey,
op: tagOperator,
value: tagValue.map((i) => i.replace(',', '')),
};
});
onChange(initialTagFilters);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tags]);
return ( return (
<Select <Select
virtual virtual
@ -60,7 +87,7 @@ function QueryBuilderSearch({ query }: QueryBuilderSearchProps): JSX.Element {
autoClearSearchValue={false} autoClearSearchValue={false}
mode="multiple" mode="multiple"
placeholder="Search Filter" placeholder="Search Filter"
value={tags} value={queryTags}
searchValue={searchValue} searchValue={searchValue}
disabled={!query.aggregateAttribute.key} disabled={!query.aggregateAttribute.key}
style={selectStyle} style={selectStyle}
@ -83,6 +110,7 @@ function QueryBuilderSearch({ query }: QueryBuilderSearchProps): JSX.Element {
interface QueryBuilderSearchProps { interface QueryBuilderSearchProps {
query: IBuilderQueryForm; query: IBuilderQueryForm;
onChange: (value: TagFilter) => void;
} }
export interface CustomTagProps { export interface CustomTagProps {

View File

@ -58,7 +58,7 @@ export const useAutoComplete = (query: IBuilderQueryForm): IAutoComplete => {
} }
return checkStringEndsWithSpace(prev) return checkStringEndsWithSpace(prev)
? `${prev} ${value}` ? `${prev} ${value}`
: `${prev}, ${value}`; : `${prev} ${value},`;
}); });
} }
if (!isMulti && isValidTag && !isExistsNotExistsOperator(value)) { if (!isMulti && isValidTag && !isExistsNotExistsOperator(value)) {

View File

@ -27,7 +27,7 @@ export const useOptions = (
); );
} else if (key && !operator) { } else if (key && !operator) {
setOptions( setOptions(
operators.map((o) => ({ operators?.map((o) => ({
value: `${key} ${o}`, value: `${key} ${o}`,
label: `${key} ${o.replace('_', ' ')}`, label: `${key} ${o.replace('_', ' ')}`,
})), })),

View File

@ -11,6 +11,7 @@ export interface IBuilderFormula {
} }
export interface TagFilterItem { export interface TagFilterItem {
id: string;
key: string; key: string;
// TODO: type it in the future // TODO: type it in the future
op: string; op: string;
@ -18,7 +19,7 @@ export interface TagFilterItem {
} }
export interface TagFilter { export interface TagFilter {
items: TagFilterItem[]; items: TagFilterItem[] | [];
// TODO: type it in the future // TODO: type it in the future
op: string; op: string;
} }
@ -35,14 +36,14 @@ export type IBuilderQuery = {
dataSource: DataSource; dataSource: DataSource;
aggregateOperator: string; aggregateOperator: string;
aggregateAttribute: string; aggregateAttribute: string;
tagFilters: TagFilter[]; tagFilters: TagFilter;
groupBy: BaseAutocompleteData[]; groupBy: BaseAutocompleteData[];
expression: string; expression: string;
disabled: boolean; disabled: boolean;
having: Having[]; having: Having[];
limit: number | null; limit: number | null;
stepInterval: number; stepInterval: number;
orderBy: string[]; orderBy: BaseAutocompleteData[];
reduceTo: string; reduceTo: string;
}; };