feat: add suggestion to order by filter (#3162)

* feat: add suggestion to order by filter

* fix: column name for order by

* fix: mapper for order by

* fix: render order by for different panels

* fix: order by timestamp and aggrigate value

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
This commit is contained in:
Yevhen Shevchenko 2023-07-19 08:47:21 +03:00 committed by GitHub
parent 5a2a987a9b
commit 98a2ef4080
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 539 additions and 279 deletions

View File

@ -0,0 +1,72 @@
import { Select, Spin } from 'antd';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import { useOrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/useOrderByFilter';
import { selectStyle } from 'container/QueryBuilder/filters/QueryBuilderSearch/config';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { memo, useMemo } from 'react';
import { StringOperators } from 'types/common/queryBuilder';
function ExplorerOrderBy({ query, onChange }: OrderByFilterProps): JSX.Element {
const {
debouncedSearchText,
selectedValue,
aggregationOptions,
generateOptions,
createOptions,
handleChange,
handleSearchKeys,
} = useOrderByFilter({ query, onChange });
const { data, isFetching } = useGetAggregateKeys(
{
aggregateAttribute: query.aggregateAttribute.key,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
searchText: debouncedSearchText,
},
{
keepPreviousData: true,
},
);
const options = useMemo(() => {
const keysOptions = createOptions(data?.payload?.attributeKeys || []);
const customOptions = createOptions([
{ key: 'timestamp', isColumn: true, type: null, dataType: null },
]);
const baseOptions = [
...customOptions,
...(query.aggregateOperator === StringOperators.NOOP
? []
: aggregationOptions),
...keysOptions,
];
return generateOptions(baseOptions);
}, [
aggregationOptions,
createOptions,
data?.payload?.attributeKeys,
generateOptions,
query.aggregateOperator,
]);
return (
<Select
mode="tags"
style={selectStyle}
onSearch={handleSearchKeys}
showSearch
showArrow={false}
value={selectedValue}
labelInValue
options={options}
notFoundContent={isFetching ? <Spin size="small" /> : null}
onChange={handleChange}
/>
);
}
export default memo(ExplorerOrderBy);

View File

@ -1,13 +1,15 @@
import { Button } from 'antd';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { QueryBuilder } from 'container/QueryBuilder';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { ButtonWrapperStyled } from 'pages/LogsExplorer/styles';
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { DataSource } from 'types/common/queryBuilder';
function LogExplorerQuerySection(): JSX.Element {
@ -23,6 +25,7 @@ function LogExplorerQuerySection(): JSX.Element {
}, [updateAllQueriesOperators]);
useShareBuilderUrl(defaultValue);
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const isTable = panelTypes === PANEL_TYPES.TABLE;
const config: QueryBuilderProps['filterConfigs'] = {
@ -32,11 +35,26 @@ function LogExplorerQuerySection(): JSX.Element {
return config;
}, [panelTypes]);
const renderOrderBy = useCallback(
({ query, onChange }: OrderByFilterProps): JSX.Element => (
<ExplorerOrderBy query={query} onChange={onChange} />
),
[],
);
const queryComponents = useMemo(
(): QueryBuilderProps['queryComponents'] => ({
...(panelTypes === PANEL_TYPES.LIST ? { renderOrderBy } : {}),
}),
[panelTypes, renderOrderBy],
);
return (
<QueryBuilder
panelType={panelTypes}
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
filterConfigs={filterConfigs}
queryComponents={queryComponents}
actions={
<ButtonWrapperStyled>
<Button type="primary" onClick={handleRunQuery}>

View File

@ -3,6 +3,7 @@ import LogDetail from 'components/LogDetail';
import TabLabel from 'components/TabLabel';
import { QueryParams } from 'constants/query';
import {
initialAutocompleteData,
initialQueriesMap,
OPERATORS,
PANEL_TYPES,
@ -17,6 +18,7 @@ import LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerList from 'container/LogsExplorerList';
import LogsExplorerTable from 'container/LogsExplorerTable';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
@ -73,6 +75,7 @@ function LogsExplorerViews(): JSX.Element {
stagedQuery,
panelType,
updateAllQueriesOperators,
updateQueriesData,
redirectWithQueryBuilderData,
} = useQueryBuilder();
@ -175,26 +178,40 @@ function LogsExplorerViews(): JSX.Element {
setActiveLog(null);
}, []);
const getUpdateQuery = useCallback(
(newPanelType: GRAPH_TYPES): Query => {
let query = updateAllQueriesOperators(
currentQuery,
newPanelType,
DataSource.TRACES,
);
if (newPanelType === PANEL_TYPES.LIST) {
query = updateQueriesData(query, 'queryData', (item) => ({
...item,
orderBy: item.orderBy.filter((item) => item.columnName !== SIGNOZ_VALUE),
aggregateAttribute: initialAutocompleteData,
}));
}
return query;
},
[currentQuery, updateAllQueriesOperators, updateQueriesData],
);
const handleChangeView = useCallback(
(newPanelType: string) => {
(type: string) => {
const newPanelType = type as GRAPH_TYPES;
if (newPanelType === panelType) return;
const query = updateAllQueriesOperators(
currentQuery,
newPanelType as GRAPH_TYPES,
DataSource.LOGS,
);
const query = getUpdateQuery(newPanelType);
redirectWithQueryBuilderData(query, {
[queryParamNamesMap.panelTypes]: newPanelType,
});
},
[
currentQuery,
panelType,
updateAllQueriesOperators,
redirectWithQueryBuilderData,
],
[panelType, getUpdateQuery, redirectWithQueryBuilderData],
);
const getRequestData = useCallback(

View File

@ -3,12 +3,12 @@ import getFromLocalstorage from 'api/browser/localstorage/get';
import setToLocalstorage from 'api/browser/localstorage/set';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import useDebounce from 'hooks/useDebounce';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueries, useQuery } from 'react-query';
import { useQueries } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
BaseAutocompleteData,
@ -116,16 +116,12 @@ const useOptionsMenu = ({
const {
data: searchedAttributesData,
isFetching: isSearchedAttributesFetching,
} = useQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, debouncedSearchText, isFocused],
async () =>
getAggregateKeys({
...initialQueryParams,
searchText: debouncedSearchText,
}),
} = useGetAggregateKeys(
{
enabled: isFocused,
...initialQueryParams,
searchText: debouncedSearchText,
},
{ queryKey: [debouncedSearchText, isFocused], enabled: isFocused },
);
const searchedAttributeKeys = useMemo(

View File

@ -3,6 +3,8 @@ import { ReactNode } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces';
export type QueryBuilderConfig =
| {
queryVariant: 'static';
@ -17,4 +19,5 @@ export type QueryBuilderProps = {
filterConfigs?: Partial<
Record<keyof IBuilderQuery, { isHidden: boolean; isDisabled: boolean }>
>;
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
};

View File

@ -16,6 +16,7 @@ export const QueryBuilder = memo(function QueryBuilder({
panelType: newPanelType,
actions,
filterConfigs = {},
queryComponents,
}: QueryBuilderProps): JSX.Element {
const {
currentQuery,
@ -74,6 +75,7 @@ export const QueryBuilder = memo(function QueryBuilder({
queryVariant={config?.queryVariant || 'dropdown'}
query={query}
filterConfigs={filterConfigs}
queryComponents={queryComponents}
/>
</Col>
))}

View File

@ -6,4 +6,4 @@ export type QueryProps = {
isAvailableToDisable: boolean;
query: IBuilderQuery;
queryVariant: 'static' | 'dropdown';
} & Pick<QueryBuilderProps, 'filterConfigs'>;
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;

View File

@ -36,6 +36,7 @@ export const Query = memo(function Query({
queryVariant,
query,
filterConfigs,
queryComponents,
}: QueryProps): JSX.Element {
const { panelType } = useQueryBuilder();
const {
@ -110,6 +111,17 @@ export const Query = memo(function Query({
[handleChangeQueryData],
);
const renderOrderByFilter = useCallback((): ReactNode => {
if (queryComponents?.renderOrderBy) {
return queryComponents.renderOrderBy({
query,
onChange: handleChangeOrderByKeys,
});
}
return <OrderByFilter query={query} onChange={handleChangeOrderByKeys} />;
}, [queryComponents, query, handleChangeOrderByKeys]);
const renderAggregateEveryFilter = useCallback(
(): JSX.Element | null =>
!filterConfigs?.stepInterval?.isHidden ? (
@ -167,9 +179,7 @@ export const Query = memo(function Query({
<Col flex="5.93rem">
<FilterLabel label="Order by" />
</Col>
<Col flex="1 1 12.5rem">
<OrderByFilter query={query} onChange={handleChangeOrderByKeys} />
</Col>
<Col flex="1 1 12.5rem">{renderOrderByFilter()}</Col>
</Row>
</Col>
)}
@ -225,9 +235,7 @@ export const Query = memo(function Query({
<Col flex="5.93rem">
<FilterLabel label="Order by" />
</Col>
<Col flex="1 1 12.5rem">
<OrderByFilter query={query} onChange={handleChangeOrderByKeys} />
</Col>
<Col flex="1 1 12.5rem">{renderOrderByFilter()}</Col>
</Row>
</Col>
@ -238,11 +246,11 @@ export const Query = memo(function Query({
}
}, [
panelType,
query,
isMetricsDataSource,
handleChangeHavingFilter,
query,
handleChangeLimit,
handleChangeOrderByKeys,
handleChangeHavingFilter,
renderOrderByFilter,
renderAggregateEveryFilter,
]);

View File

@ -7,6 +7,7 @@ import {
selectValueDivider,
} from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import useDebounce from 'hooks/useDebounce';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
// ** Components
@ -14,7 +15,7 @@ import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAut
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import { isEqual, uniqWith } from 'lodash-es';
import { memo, useCallback, useEffect, useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { useQueryClient } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { SelectOption } from 'types/common/select';
@ -38,16 +39,15 @@ export const GroupByFilter = memo(function GroupByFilter({
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
const { isFetching } = useQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, debouncedValue, isFocused],
async () =>
getAggregateKeys({
aggregateAttribute: query.aggregateAttribute.key,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
searchText: debouncedValue,
}),
const { isFetching } = useGetAggregateKeys(
{
aggregateAttribute: query.aggregateAttribute.key,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
searchText: debouncedValue,
},
{
queryKey: [debouncedValue, isFocused],
enabled: !disabled && isFocused,
onSuccess: (data) => {
const keys = query.groupBy.reduce<string[]>((acc, item) => {

View File

@ -1,208 +1,57 @@
import { Select, Spin } from 'antd';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { IOption } from 'hooks/useResourceAttribute/types';
import { uniqWith } from 'lodash-es';
import * as Papa from 'papaparse';
import { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useMemo } from 'react';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { selectStyle } from '../QueryBuilderSearch/config';
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
import { FILTERS } from './config';
import { OrderByFilterProps } from './OrderByFilter.interfaces';
import {
checkIfKeyPresent,
getLabelFromValue,
mapLabelValuePairs,
orderByValueDelimiter,
splitOrderByFromString,
transformToOrderByStringValues,
} from './utils';
import { useOrderByFilter } from './useOrderByFilter';
export function OrderByFilter({
query,
onChange,
}: OrderByFilterProps): JSX.Element {
const [searchText, setSearchText] = useState<string>('');
const [selectedValue, setSelectedValue] = useState<IOption[]>(
transformToOrderByStringValues(query.orderBy),
const {
debouncedSearchText,
selectedValue,
aggregationOptions,
generateOptions,
createOptions,
handleChange,
handleSearchKeys,
} = useOrderByFilter({ query, onChange });
const { data, isFetching } = useGetAggregateKeys(
{
aggregateAttribute: query.aggregateAttribute.key,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
searchText: debouncedSearchText,
},
{
enabled: !!query.aggregateAttribute.key,
keepPreviousData: true,
},
);
const { data, isFetching } = useQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
async () =>
getAggregateKeys({
aggregateAttribute: query.aggregateAttribute.key,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
searchText,
}),
{ enabled: !!query.aggregateAttribute.key, keepPreviousData: true },
);
const handleSearchKeys = useCallback(
(searchText: string): void => setSearchText(searchText),
[],
);
const noAggregationOptions = useMemo(
() =>
data?.payload?.attributeKeys
? mapLabelValuePairs(data?.payload?.attributeKeys).flat()
: [],
[data?.payload?.attributeKeys],
);
const aggregationOptions = useMemo(
() =>
mapLabelValuePairs(query.groupBy)
.flat()
.concat([
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.ASC}`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}${FILTERS.ASC}`,
},
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.DESC}`,
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}${FILTERS.DESC}`,
},
]),
[query.aggregateAttribute.key, query.aggregateOperator, query.groupBy],
);
const customValue: IOption[] = useMemo(() => {
if (!searchText) return [];
return [
{
label: `${searchText} ${FILTERS.ASC}`,
value: `${searchText}${orderByValueDelimiter}${FILTERS.ASC}`,
},
{
label: `${searchText} ${FILTERS.DESC}`,
value: `${searchText}${orderByValueDelimiter}${FILTERS.DESC}`,
},
];
}, [searchText]);
const optionsData = useMemo(() => {
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
const groupByOptions = createOptions(query.groupBy);
const options =
query.aggregateOperator === MetricAggregateOperator.NOOP
? noAggregationOptions
: aggregationOptions;
? keyOptions
: [...groupByOptions, ...aggregationOptions];
const resultOption = [...customValue, ...options];
return resultOption.filter(
(option) =>
!getLabelFromValue(selectedValue).includes(
getRemoveOrderFromValue(option.value),
),
);
return generateOptions(options);
}, [
aggregationOptions,
customValue,
noAggregationOptions,
createOptions,
data?.payload?.attributeKeys,
generateOptions,
query.aggregateOperator,
selectedValue,
query.groupBy,
]);
const getUniqValues = useCallback((values: IOption[]): IOption[] => {
const modifiedValues = values.map((item) => {
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
if (!match) return { label: item.label, value: item.value };
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
const [_, order] = match.data.flat() as string[];
if (order)
return {
label: item.label,
value: item.value,
};
return {
label: `${item.value} ${FILTERS.ASC}`,
value: `${item.value}${orderByValueDelimiter}${FILTERS.ASC}`,
};
});
return uniqWith(
modifiedValues,
(current, next) =>
getRemoveOrderFromValue(current.value) ===
getRemoveOrderFromValue(next.value),
);
}, []);
const getValidResult = useCallback(
(result: IOption[]): IOption[] =>
result.reduce<IOption[]>((acc, item) => {
if (item.value === FILTERS.ASC || item.value === FILTERS.DESC) return acc;
if (item.value.includes(FILTERS.ASC) || item.value.includes(FILTERS.DESC)) {
const splittedOrderBy = splitOrderByFromString(item.value);
if (splittedOrderBy) {
acc.push({
label: `${splittedOrderBy.columnName} ${splittedOrderBy.order}`,
value: `${splittedOrderBy.columnName}${orderByValueDelimiter}${splittedOrderBy.order}`,
});
return acc;
}
}
acc.push(item);
return acc;
}, []),
[],
);
const handleChange = (values: IOption[]): void => {
const validResult = getValidResult(values);
const result = getUniqValues(validResult);
const orderByValues: OrderByPayload[] = result.map((item) => {
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
if (!match) {
return {
columnName: item.value,
order: 'asc',
};
}
const [columnName, order] = match.data.flat() as string[];
const columnNameValue = checkIfKeyPresent(
columnName,
query.aggregateAttribute.key,
)
? '#SIGNOZ_VALUE'
: columnName;
const orderValue = order ?? 'asc';
return {
columnName: columnNameValue,
order: orderValue,
};
});
const selectedValue: IOption[] = orderByValues.map((item) => ({
label: `${item.columnName} ${item.order}`,
value: `${item.columnName} ${item.order}`,
}));
setSelectedValue(selectedValue);
setSearchText('');
onChange(orderByValues);
};
const isDisabledSelect = useMemo(
() =>
!query.aggregateAttribute.key ||

View File

@ -0,0 +1 @@
export const SIGNOZ_VALUE = '#SIGNOZ_VALUE';

View File

@ -0,0 +1,199 @@
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import useDebounce from 'hooks/useDebounce';
import { IOption } from 'hooks/useResourceAttribute/types';
import { isEqual, uniqWith } from 'lodash-es';
import * as Papa from 'papaparse';
import { useCallback, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
import { FILTERS } from './config';
import { SIGNOZ_VALUE } from './constants';
import { OrderByFilterProps } from './OrderByFilter.interfaces';
import {
getLabelFromValue,
mapLabelValuePairs,
orderByValueDelimiter,
splitOrderByFromString,
transformToOrderByStringValues,
} from './utils';
type UseOrderByFilterResult = {
searchText: string;
debouncedSearchText: string;
selectedValue: IOption[];
aggregationOptions: IOption[];
generateOptions: (options: IOption[]) => IOption[];
createOptions: (data: BaseAutocompleteData[]) => IOption[];
handleChange: (values: IOption[]) => void;
handleSearchKeys: (search: string) => void;
};
export const useOrderByFilter = ({
query,
onChange,
}: OrderByFilterProps): UseOrderByFilterResult => {
const [searchText, setSearchText] = useState<string>('');
const debouncedSearchText = useDebounce(searchText, DEBOUNCE_DELAY);
const handleSearchKeys = useCallback(
(searchText: string): void => setSearchText(searchText),
[],
);
const getUniqValues = useCallback((values: IOption[]): IOption[] => {
const modifiedValues = values.map((item) => {
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
if (!match) return { label: item.label, value: item.value };
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
const [_, order] = match.data.flat() as string[];
if (order)
return {
label: item.label,
value: item.value,
};
return {
label: `${item.value} ${FILTERS.ASC}`,
value: `${item.value}${orderByValueDelimiter}${FILTERS.ASC}`,
};
});
return uniqWith(
modifiedValues,
(current, next) =>
getRemoveOrderFromValue(current.value) ===
getRemoveOrderFromValue(next.value),
);
}, []);
const customValue: IOption[] = useMemo(() => {
if (!searchText) return [];
return [
{
label: `${searchText} ${FILTERS.ASC}`,
value: `${searchText}${orderByValueDelimiter}${FILTERS.ASC}`,
},
{
label: `${searchText} ${FILTERS.DESC}`,
value: `${searchText}${orderByValueDelimiter}${FILTERS.DESC}`,
},
];
}, [searchText]);
const selectedValue = useMemo(() => transformToOrderByStringValues(query), [
query,
]);
const generateOptions = useCallback(
(options: IOption[]): IOption[] => {
const currentCustomValue = options.find(
(keyOption) =>
getRemoveOrderFromValue(keyOption.value) === debouncedSearchText,
)
? []
: customValue;
const result = [...currentCustomValue, ...options];
const uniqResult = uniqWith(result, isEqual);
return uniqResult.filter(
(option) =>
!getLabelFromValue(selectedValue).includes(
getRemoveOrderFromValue(option.value),
),
);
},
[customValue, debouncedSearchText, selectedValue],
);
const getValidResult = useCallback(
(result: IOption[]): IOption[] =>
result.reduce<IOption[]>((acc, item) => {
if (item.value === FILTERS.ASC || item.value === FILTERS.DESC) return acc;
if (item.value.includes(FILTERS.ASC) || item.value.includes(FILTERS.DESC)) {
const splittedOrderBy = splitOrderByFromString(item.value);
if (splittedOrderBy) {
acc.push({
label: `${splittedOrderBy.columnName} ${splittedOrderBy.order}`,
value: `${splittedOrderBy.columnName}${orderByValueDelimiter}${splittedOrderBy.order}`,
});
return acc;
}
}
acc.push(item);
return acc;
}, []),
[],
);
const handleChange = (values: IOption[]): void => {
const validResult = getValidResult(values);
const result = getUniqValues(validResult);
const orderByValues: OrderByPayload[] = result.map((item) => {
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
if (!match) {
return {
columnName: item.value,
order: 'asc',
};
}
const [columnName, order] = match.data.flat() as string[];
const columnNameValue =
columnName === SIGNOZ_VALUE ? SIGNOZ_VALUE : columnName;
const orderValue = order ?? 'asc';
return {
columnName: columnNameValue,
order: orderValue,
};
});
setSearchText('');
onChange(orderByValues);
};
const createOptions = useCallback(
(data: BaseAutocompleteData[]): IOption[] => mapLabelValuePairs(data).flat(),
[],
);
const aggregationOptions = useMemo(
() => [
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.ASC}`,
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${FILTERS.ASC}`,
},
{
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.DESC}`,
value: `${SIGNOZ_VALUE}${orderByValueDelimiter}${FILTERS.DESC}`,
},
],
[query],
);
return {
searchText,
debouncedSearchText,
selectedValue,
aggregationOptions,
createOptions,
handleChange,
handleSearchKeys,
generateOptions,
};
};

View File

@ -1,31 +1,32 @@
import { IOption } from 'hooks/useResourceAttribute/types';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import * as Papa from 'papaparse';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
OrderByPayload,
} from 'types/api/queryBuilder/queryBuilderData';
import { FILTERS } from './config';
import { SIGNOZ_VALUE } from './constants';
export const orderByValueDelimiter = '|';
export const transformToOrderByStringValues = (
orderBy: OrderByPayload[],
query: IBuilderQuery,
): IOption[] => {
const prepareSelectedValue: IOption[] = orderBy.reduce<IOption[]>(
(acc, item) => {
if (item.columnName === '#SIGNOZ_VALUE') return acc;
const option: IOption = {
label: `${item.columnName} ${item.order}`,
const prepareSelectedValue: IOption[] = query.orderBy.map((item) => {
if (item.columnName === SIGNOZ_VALUE) {
return {
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
};
}
acc.push(option);
return acc;
},
[],
);
return {
label: `${item.columnName} ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
};
});
return prepareSelectedValue;
};
@ -34,20 +35,15 @@ 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}${orderByValueDelimiter}asc`,
label: `${value} ${FILTERS.ASC}`,
value: `${value}${orderByValueDelimiter}${FILTERS.ASC}`,
},
{
label: `${label} desc`,
value: `${value}${orderByValueDelimiter}desc`,
label: `${value} ${FILTERS.DESC}`,
value: `${value}${orderByValueDelimiter}${FILTERS.DESC}`,
},
];
});
@ -58,6 +54,7 @@ export function getLabelFromValue(arr: IOption[]): string[] {
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
if (match) {
const [key] = match.data as string[];
return key[0];
}

View File

@ -1,10 +1,12 @@
import { Button } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { QueryBuilder } from 'container/QueryBuilder';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { ButtonWrapper, Container } from './styles';
@ -22,6 +24,22 @@ function QuerySection(): JSX.Element {
return config;
}, []);
const renderOrderBy = useCallback(
({ query, onChange }: OrderByFilterProps) => (
<ExplorerOrderBy query={query} onChange={onChange} />
),
[],
);
const queryComponents = useMemo((): QueryBuilderProps['queryComponents'] => {
const shouldRenderCustomOrderBy =
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE;
return {
...(shouldRenderCustomOrderBy ? { renderOrderBy } : {}),
};
}, [panelTypes, renderOrderBy]);
return (
<Container>
<QueryBuilder
@ -31,6 +49,7 @@ function QuerySection(): JSX.Element {
initialDataSource: DataSource.TRACES,
}}
filterConfigs={filterConfigs}
queryComponents={queryComponents}
actions={
<ButtonWrapper>
<Button onClick={handleRunQuery} type="primary">

View File

@ -1,6 +1,4 @@
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import {
getRemovePrefixFromKey,
@ -10,12 +8,13 @@ import {
import useDebounceValue from 'hooks/useDebounce';
import { isEqual, uniqWith } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useDebounce } from 'react-use';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { useGetAggregateKeys } from './useGetAggregateKeys';
type IuseFetchKeysAndValues = {
keys: BaseAutocompleteData[];
results: string[];
@ -71,19 +70,15 @@ export const useFetchKeysAndValues = (
],
);
const { data, isFetching, status } = useQuery(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchParams],
async () =>
getAggregateKeys({
searchText: searchKey,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
tagType: query.aggregateAttribute.type ?? null,
}),
const { data, isFetching, status } = useGetAggregateKeys(
{
enabled: isQueryEnabled,
searchText: searchKey,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
tagType: query.aggregateAttribute.type ?? null,
},
{ queryKey: [searchParams], enabled: isQueryEnabled },
);
/**

View File

@ -0,0 +1,34 @@
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { IGetAttributeKeysPayload } from 'types/api/queryBuilder/getAttributeKeys';
import { IQueryAutocompleteResponse } from 'types/api/queryBuilder/queryAutocompleteResponse';
type UseGetAttributeKeys = (
requestData: IGetAttributeKeysPayload,
options?: UseQueryOptions<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>,
) => UseQueryResult<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
>;
export const useGetAggregateKeys: UseGetAttributeKeys = (
requestData,
options,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, ...options.queryKey];
}
return [QueryBuilderKeys.GET_AGGREGATE_KEYS, requestData];
}, [options?.queryKey, requestData]);
return useQuery<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse>({
queryKey,
queryFn: () => getAggregateKeys(requestData),
...options,
});
};

View File

@ -2,11 +2,16 @@ import { Tabs } from 'antd';
import axios from 'axios';
import ExplorerCard from 'components/ExplorerCard';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import {
initialAutocompleteData,
initialQueriesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes';
import ExportPanel from 'container/ExportPanel';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
import QuerySection from 'container/TracesExplorer/QuerySection';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
@ -17,6 +22,7 @@ import history from 'lib/history';
import { useCallback, useEffect, useMemo } from 'react';
import { generatePath } from 'react-router-dom';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { ActionsWrapper, Container } from './styles';
@ -29,6 +35,7 @@ function TracesExplorer(): JSX.Element {
currentQuery,
panelType,
updateAllQueriesOperators,
updateQueriesData,
redirectWithQueryBuilderData,
} = useQueryBuilder();
@ -141,26 +148,42 @@ function TracesExplorer(): JSX.Element {
[exportDefaultQuery, notifications, updateDashboard],
);
const handleTabChange = useCallback(
(newPanelType: string): void => {
if (panelType === newPanelType) return;
const query = updateAllQueriesOperators(
const getUpdateQuery = useCallback(
(newPanelType: GRAPH_TYPES): Query => {
let query = updateAllQueriesOperators(
currentQuery,
newPanelType as GRAPH_TYPES,
newPanelType,
DataSource.TRACES,
);
if (
newPanelType === PANEL_TYPES.LIST ||
newPanelType === PANEL_TYPES.TRACE
) {
query = updateQueriesData(query, 'queryData', (item) => ({
...item,
orderBy: item.orderBy.filter((item) => item.columnName !== SIGNOZ_VALUE),
aggregateAttribute: initialAutocompleteData,
}));
}
return query;
},
[currentQuery, updateAllQueriesOperators, updateQueriesData],
);
const handleTabChange = useCallback(
(type: string): void => {
const newPanelType = type as GRAPH_TYPES;
if (panelType === newPanelType) return;
const query = getUpdateQuery(newPanelType);
redirectWithQueryBuilderData(query, {
[queryParamNamesMap.panelTypes]: newPanelType,
});
},
[
currentQuery,
panelType,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
],
[getUpdateQuery, panelType, redirectWithQueryBuilderData],
);
useShareBuilderUrl(defaultQuery);

View File

@ -70,6 +70,7 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
handleRunQuery: () => {},
resetStagedQuery: () => {},
updateAllQueriesOperators: () => initialQueriesMap.metrics,
updateQueriesData: () => initialQueriesMap.metrics,
initQueryBuilderData: () => {},
});
@ -222,6 +223,22 @@ export function QueryBuilderProvider({
[getElementWithActualOperator],
);
const updateQueriesData = useCallback(
<T extends keyof QueryBuilderData>(
query: Query,
type: T,
updateCallback: (
item: QueryBuilderData[T][number],
index: number,
) => QueryBuilderData[T][number],
): Query => {
const result = query.builder[type].map(updateCallback);
return { ...query, builder: { ...query.builder, [type]: result } };
},
[],
);
const removeQueryBuilderEntityByIndex = useCallback(
(type: keyof QueryBuilderData, index: number) => {
setCurrentQuery((prevState) => {
@ -567,6 +584,7 @@ export function QueryBuilderProvider({
handleRunQuery,
resetStagedQuery,
updateAllQueriesOperators,
updateQueriesData,
initQueryBuilderData,
}),
[
@ -588,6 +606,7 @@ export function QueryBuilderProvider({
handleRunQuery,
resetStagedQuery,
updateAllQueriesOperators,
updateQueriesData,
initQueryBuilderData,
],
);

View File

@ -192,6 +192,14 @@ export type QueryBuilderContextType = {
panelType: GRAPH_TYPES,
dataSource: DataSource,
) => Query;
updateQueriesData: <T extends keyof QueryBuilderData>(
query: Query,
type: T,
updateCallback: (
item: QueryBuilderData[T][number],
index: number,
) => QueryBuilderData[T][number],
) => Query;
initQueryBuilderData: (query: Query) => void;
};