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

View File

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

View File

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

View File

@ -3,6 +3,8 @@ import { ReactNode } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces';
export type QueryBuilderConfig = export type QueryBuilderConfig =
| { | {
queryVariant: 'static'; queryVariant: 'static';
@ -17,4 +19,5 @@ export type QueryBuilderProps = {
filterConfigs?: Partial< filterConfigs?: Partial<
Record<keyof IBuilderQuery, { isHidden: boolean; isDisabled: boolean }> 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, panelType: newPanelType,
actions, actions,
filterConfigs = {}, filterConfigs = {},
queryComponents,
}: QueryBuilderProps): JSX.Element { }: QueryBuilderProps): JSX.Element {
const { const {
currentQuery, currentQuery,
@ -74,6 +75,7 @@ export const QueryBuilder = memo(function QueryBuilder({
queryVariant={config?.queryVariant || 'dropdown'} queryVariant={config?.queryVariant || 'dropdown'}
query={query} query={query}
filterConfigs={filterConfigs} filterConfigs={filterConfigs}
queryComponents={queryComponents}
/> />
</Col> </Col>
))} ))}

View File

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

View File

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

View File

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

View File

@ -1,208 +1,57 @@
import { Select, Spin } from 'antd'; import { Select, Spin } from 'antd';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys'; import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder'; import { useMemo } from 'react';
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 { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder'; import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { selectStyle } from '../QueryBuilderSearch/config'; import { selectStyle } from '../QueryBuilderSearch/config';
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
import { FILTERS } from './config';
import { OrderByFilterProps } from './OrderByFilter.interfaces'; import { OrderByFilterProps } from './OrderByFilter.interfaces';
import { import { useOrderByFilter } from './useOrderByFilter';
checkIfKeyPresent,
getLabelFromValue,
mapLabelValuePairs,
orderByValueDelimiter,
splitOrderByFromString,
transformToOrderByStringValues,
} from './utils';
export function OrderByFilter({ export function OrderByFilter({
query, query,
onChange, onChange,
}: OrderByFilterProps): JSX.Element { }: OrderByFilterProps): JSX.Element {
const [searchText, setSearchText] = useState<string>(''); const {
const [selectedValue, setSelectedValue] = useState<IOption[]>( debouncedSearchText,
transformToOrderByStringValues(query.orderBy), 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 optionsData = useMemo(() => {
const keyOptions = createOptions(data?.payload?.attributeKeys || []);
const groupByOptions = createOptions(query.groupBy);
const options = const options =
query.aggregateOperator === MetricAggregateOperator.NOOP query.aggregateOperator === MetricAggregateOperator.NOOP
? noAggregationOptions ? keyOptions
: aggregationOptions; : [...groupByOptions, ...aggregationOptions];
const resultOption = [...customValue, ...options]; return generateOptions(options);
return resultOption.filter(
(option) =>
!getLabelFromValue(selectedValue).includes(
getRemoveOrderFromValue(option.value),
),
);
}, [ }, [
aggregationOptions, aggregationOptions,
customValue, createOptions,
noAggregationOptions, data?.payload?.attributeKeys,
generateOptions,
query.aggregateOperator, 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( const isDisabledSelect = useMemo(
() => () =>
!query.aggregateAttribute.key || !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 { IOption } from 'hooks/useResourceAttribute/types';
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import * as Papa from 'papaparse'; import * as Papa from 'papaparse';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; 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 { FILTERS } from './config';
import { SIGNOZ_VALUE } from './constants';
export const orderByValueDelimiter = '|'; export const orderByValueDelimiter = '|';
export const transformToOrderByStringValues = ( export const transformToOrderByStringValues = (
orderBy: OrderByPayload[], query: IBuilderQuery,
): IOption[] => { ): IOption[] => {
const prepareSelectedValue: IOption[] = orderBy.reduce<IOption[]>( const prepareSelectedValue: IOption[] = query.orderBy.map((item) => {
(acc, item) => { if (item.columnName === SIGNOZ_VALUE) {
if (item.columnName === '#SIGNOZ_VALUE') return acc; return {
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${item.order}`,
const option: IOption = {
label: `${item.columnName} ${item.order}`,
value: `${item.columnName}${orderByValueDelimiter}${item.order}`, value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
}; };
}
acc.push(option); return {
label: `${item.columnName} ${item.order}`,
return acc; value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
}, };
[], });
);
return prepareSelectedValue; return prepareSelectedValue;
}; };
@ -34,20 +35,15 @@ export function mapLabelValuePairs(
arr: BaseAutocompleteData[], arr: BaseAutocompleteData[],
): Array<IOption>[] { ): Array<IOption>[] {
return arr.map((item) => { return arr.map((item) => {
const label = transformStringWithPrefix({
str: item.key,
prefix: item.type || '',
condition: !item.isColumn,
});
const value = item.key; const value = item.key;
return [ return [
{ {
label: `${label} asc`, label: `${value} ${FILTERS.ASC}`,
value: `${value}${orderByValueDelimiter}asc`, value: `${value}${orderByValueDelimiter}${FILTERS.ASC}`,
}, },
{ {
label: `${label} desc`, label: `${value} ${FILTERS.DESC}`,
value: `${value}${orderByValueDelimiter}desc`, value: `${value}${orderByValueDelimiter}${FILTERS.DESC}`,
}, },
]; ];
}); });
@ -58,6 +54,7 @@ export function getLabelFromValue(arr: IOption[]): string[] {
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter }); const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
if (match) { if (match) {
const [key] = match.data as string[]; const [key] = match.data as string[];
return key[0]; return key[0];
} }

View File

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

View File

@ -1,6 +1,4 @@
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues'; import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { import {
getRemovePrefixFromKey, getRemovePrefixFromKey,
@ -10,12 +8,13 @@ import {
import useDebounceValue from 'hooks/useDebounce'; import useDebounceValue from 'hooks/useDebounce';
import { isEqual, uniqWith } from 'lodash-es'; import { isEqual, uniqWith } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from 'react-query';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { useGetAggregateKeys } from './useGetAggregateKeys';
type IuseFetchKeysAndValues = { type IuseFetchKeysAndValues = {
keys: BaseAutocompleteData[]; keys: BaseAutocompleteData[];
results: string[]; results: string[];
@ -71,19 +70,15 @@ export const useFetchKeysAndValues = (
], ],
); );
const { data, isFetching, status } = useQuery( const { data, isFetching, status } = useGetAggregateKeys(
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchParams],
async () =>
getAggregateKeys({
searchText: searchKey,
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator,
aggregateAttribute: query.aggregateAttribute.key,
tagType: query.aggregateAttribute.type ?? null,
}),
{ {
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 axios from 'axios';
import ExplorerCard from 'components/ExplorerCard'; import ExplorerCard from 'components/ExplorerCard';
import { QueryParams } from 'constants/query'; 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 { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import ExportPanel from 'container/ExportPanel'; import ExportPanel from 'container/ExportPanel';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
import QuerySection from 'container/TracesExplorer/QuerySection'; import QuerySection from 'container/TracesExplorer/QuerySection';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils'; import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
@ -17,6 +22,7 @@ import history from 'lib/history';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { generatePath } from 'react-router-dom'; import { generatePath } from 'react-router-dom';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { ActionsWrapper, Container } from './styles'; import { ActionsWrapper, Container } from './styles';
@ -29,6 +35,7 @@ function TracesExplorer(): JSX.Element {
currentQuery, currentQuery,
panelType, panelType,
updateAllQueriesOperators, updateAllQueriesOperators,
updateQueriesData,
redirectWithQueryBuilderData, redirectWithQueryBuilderData,
} = useQueryBuilder(); } = useQueryBuilder();
@ -141,26 +148,42 @@ function TracesExplorer(): JSX.Element {
[exportDefaultQuery, notifications, updateDashboard], [exportDefaultQuery, notifications, updateDashboard],
); );
const handleTabChange = useCallback( const getUpdateQuery = useCallback(
(newPanelType: string): void => { (newPanelType: GRAPH_TYPES): Query => {
if (panelType === newPanelType) return; let query = updateAllQueriesOperators(
const query = updateAllQueriesOperators(
currentQuery, currentQuery,
newPanelType as GRAPH_TYPES, newPanelType,
DataSource.TRACES, 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, { redirectWithQueryBuilderData(query, {
[queryParamNamesMap.panelTypes]: newPanelType, [queryParamNamesMap.panelTypes]: newPanelType,
}); });
}, },
[ [getUpdateQuery, panelType, redirectWithQueryBuilderData],
currentQuery,
panelType,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
],
); );
useShareBuilderUrl(defaultQuery); useShareBuilderUrl(defaultQuery);

View File

@ -70,6 +70,7 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
handleRunQuery: () => {}, handleRunQuery: () => {},
resetStagedQuery: () => {}, resetStagedQuery: () => {},
updateAllQueriesOperators: () => initialQueriesMap.metrics, updateAllQueriesOperators: () => initialQueriesMap.metrics,
updateQueriesData: () => initialQueriesMap.metrics,
initQueryBuilderData: () => {}, initQueryBuilderData: () => {},
}); });
@ -222,6 +223,22 @@ export function QueryBuilderProvider({
[getElementWithActualOperator], [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( const removeQueryBuilderEntityByIndex = useCallback(
(type: keyof QueryBuilderData, index: number) => { (type: keyof QueryBuilderData, index: number) => {
setCurrentQuery((prevState) => { setCurrentQuery((prevState) => {
@ -567,6 +584,7 @@ export function QueryBuilderProvider({
handleRunQuery, handleRunQuery,
resetStagedQuery, resetStagedQuery,
updateAllQueriesOperators, updateAllQueriesOperators,
updateQueriesData,
initQueryBuilderData, initQueryBuilderData,
}), }),
[ [
@ -588,6 +606,7 @@ export function QueryBuilderProvider({
handleRunQuery, handleRunQuery,
resetStagedQuery, resetStagedQuery,
updateAllQueriesOperators, updateAllQueriesOperators,
updateQueriesData,
initQueryBuilderData, initQueryBuilderData,
], ],
); );

View File

@ -192,6 +192,14 @@ export type QueryBuilderContextType = {
panelType: GRAPH_TYPES, panelType: GRAPH_TYPES,
dataSource: DataSource, dataSource: DataSource,
) => Query; ) => 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; initQueryBuilderData: (query: Query) => void;
}; };