mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 04:35:58 +08:00
fix: fix initial columns for the list view / fix attributes API call (#3056)
* fix: fix initial columns for the list view * fix: replace columns * fix: update attribute api call for the options menu * fix: update call attributes API call * fix: add error msg for the options list
This commit is contained in:
parent
1295e179b2
commit
857e505323
@ -1,5 +1,5 @@
|
|||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { Input } from 'antd';
|
import { Input, Spin } from 'antd';
|
||||||
import Typography from 'antd/es/typography/Typography';
|
import Typography from 'antd/es/typography/Typography';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -26,12 +26,17 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
|||||||
|
|
||||||
<Input.Group compact>
|
<Input.Group compact>
|
||||||
<AddColumnSelect
|
<AddColumnSelect
|
||||||
|
loading={config.isFetching}
|
||||||
size="small"
|
size="small"
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
options={config.options}
|
options={config.options}
|
||||||
value={[]}
|
value={[]}
|
||||||
onChange={config.onChange}
|
onSelect={config.onSelect}
|
||||||
|
onSearch={config.onSearch}
|
||||||
|
onFocus={config.onFocus}
|
||||||
|
onBlur={config.onBlur}
|
||||||
|
notFoundContent={config.isFetching ? <Spin size="small" /> : null}
|
||||||
/>
|
/>
|
||||||
<SearchIconWrapper $isDarkMode={isDarkMode}>
|
<SearchIconWrapper $isDarkMode={isDarkMode}>
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
|
@ -16,7 +16,11 @@ export interface InitialOptions
|
|||||||
export type OptionsMenuConfig = {
|
export type OptionsMenuConfig = {
|
||||||
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
||||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||||
addColumn?: Pick<SelectProps, 'options' | 'onChange'> & {
|
addColumn?: Pick<
|
||||||
|
SelectProps,
|
||||||
|
'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur'
|
||||||
|
> & {
|
||||||
|
isFetching: boolean;
|
||||||
value: BaseAutocompleteData[];
|
value: BaseAutocompleteData[];
|
||||||
onRemove: (key: string) => void;
|
onRemove: (key: string) => void;
|
||||||
};
|
};
|
||||||
|
@ -4,15 +4,21 @@ 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 { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||||
|
import useDebounce from 'hooks/useDebounce';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQueries, useQuery } from 'react-query';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
BaseAutocompleteData,
|
||||||
|
IQueryAutocompleteResponse,
|
||||||
|
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { defaultOptionsQuery, URL_OPTIONS } from './constants';
|
import { defaultOptionsQuery, URL_OPTIONS } from './constants';
|
||||||
import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types';
|
import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types';
|
||||||
import { getInitialColumns, getOptionsFromKeys } from './utils';
|
import { getOptionsFromKeys } from './utils';
|
||||||
|
|
||||||
interface UseOptionsMenuProps {
|
interface UseOptionsMenuProps {
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
@ -21,7 +27,6 @@ interface UseOptionsMenuProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UseOptionsMenu {
|
interface UseOptionsMenu {
|
||||||
isLoading: boolean;
|
|
||||||
options: OptionsQuery;
|
options: OptionsQuery;
|
||||||
config: OptionsMenuConfig;
|
config: OptionsMenuConfig;
|
||||||
}
|
}
|
||||||
@ -31,41 +36,108 @@ const useOptionsMenu = ({
|
|||||||
aggregateOperator,
|
aggregateOperator,
|
||||||
initialOptions = {},
|
initialOptions = {},
|
||||||
}: UseOptionsMenuProps): UseOptionsMenu => {
|
}: UseOptionsMenuProps): UseOptionsMenu => {
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||||
|
const debouncedSearchText = useDebounce(searchText, 300);
|
||||||
|
|
||||||
const localStorageOptionsQuery = getFromLocalstorage(
|
const localStorageOptionsQuery = getFromLocalstorage(
|
||||||
LOCALSTORAGE.LIST_OPTIONS,
|
LOCALSTORAGE.LIST_OPTIONS,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const initialQueryParams = useMemo(
|
||||||
|
() => ({
|
||||||
|
searchText: '',
|
||||||
|
aggregateAttribute: '',
|
||||||
|
tagType: null,
|
||||||
|
dataSource,
|
||||||
|
aggregateOperator,
|
||||||
|
}),
|
||||||
|
[dataSource, aggregateOperator],
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
query: optionsQuery,
|
query: optionsQuery,
|
||||||
queryData: optionsQueryData,
|
queryData: optionsQueryData,
|
||||||
redirectWithQuery: redirectWithOptionsData,
|
redirectWithQuery: redirectWithOptionsData,
|
||||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||||
|
|
||||||
const { data, isFetched, isLoading } = useQuery(
|
const initialQueries = useMemo(
|
||||||
[QueryBuilderKeys.GET_ATTRIBUTE_KEY, dataSource, aggregateOperator],
|
() =>
|
||||||
async () =>
|
initialOptions?.selectColumns?.map((column) => ({
|
||||||
getAggregateKeys({
|
queryKey: column,
|
||||||
searchText: '',
|
queryFn: (): Promise<
|
||||||
dataSource,
|
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||||
aggregateOperator,
|
> =>
|
||||||
aggregateAttribute: '',
|
getAggregateKeys({
|
||||||
tagType: null,
|
...initialQueryParams,
|
||||||
}),
|
searchText: column,
|
||||||
|
}),
|
||||||
|
enabled: !!column && !optionsQuery,
|
||||||
|
})) || [],
|
||||||
|
[initialOptions?.selectColumns, initialQueryParams, optionsQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
const attributeKeys = useMemo(() => data?.payload?.attributeKeys || [], [
|
const initialAttributesResult = useQueries(initialQueries);
|
||||||
data?.payload?.attributeKeys,
|
|
||||||
|
const isFetchedInitialAttributes = useMemo(
|
||||||
|
() => initialAttributesResult.every((result) => result.isFetched),
|
||||||
|
[initialAttributesResult],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialSelectedColumns = useMemo(() => {
|
||||||
|
if (!isFetchedInitialAttributes) return [];
|
||||||
|
|
||||||
|
const attributesData = initialAttributesResult?.reduce(
|
||||||
|
(acc, attributeResponse) => {
|
||||||
|
const data = attributeResponse?.data?.payload?.attributeKeys || [];
|
||||||
|
|
||||||
|
return [...acc, ...data];
|
||||||
|
},
|
||||||
|
[] as BaseAutocompleteData[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
(initialOptions.selectColumns
|
||||||
|
?.map((column) => attributesData.find(({ key }) => key === column))
|
||||||
|
.filter(Boolean) as BaseAutocompleteData[]) || []
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
isFetchedInitialAttributes,
|
||||||
|
initialOptions?.selectColumns,
|
||||||
|
initialAttributesResult,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: searchedAttributesData,
|
||||||
|
isFetching: isSearchedAttributesFetching,
|
||||||
|
} = useQuery(
|
||||||
|
[QueryBuilderKeys.GET_AGGREGATE_KEYS, debouncedSearchText, isFocused],
|
||||||
|
async () =>
|
||||||
|
getAggregateKeys({
|
||||||
|
...initialQueryParams,
|
||||||
|
searchText: debouncedSearchText,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
enabled: isFocused,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchedAttributeKeys = useMemo(
|
||||||
|
() => searchedAttributesData?.payload?.attributeKeys || [],
|
||||||
|
[searchedAttributesData?.payload?.attributeKeys],
|
||||||
|
);
|
||||||
|
|
||||||
const initialOptionsQuery: OptionsQuery = useMemo(
|
const initialOptionsQuery: OptionsQuery = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
...defaultOptionsQuery,
|
...defaultOptionsQuery,
|
||||||
...initialOptions,
|
...initialOptions,
|
||||||
selectColumns: initialOptions?.selectColumns
|
selectColumns: initialOptions?.selectColumns
|
||||||
? getInitialColumns(initialOptions?.selectColumns || [], attributeKeys)
|
? initialSelectedColumns
|
||||||
: defaultOptionsQuery.selectColumns,
|
: defaultOptionsQuery.selectColumns,
|
||||||
}),
|
}),
|
||||||
[initialOptions, attributeKeys],
|
[initialOptions, initialSelectedColumns],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedColumnKeys = useMemo(
|
const selectedColumnKeys = useMemo(
|
||||||
@ -73,13 +145,13 @@ const useOptionsMenu = ({
|
|||||||
[optionsQueryData],
|
[optionsQueryData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addColumnOptions = useMemo(() => {
|
const optionsFromAttributeKeys = useMemo(() => {
|
||||||
const filteredAttributeKeys = attributeKeys.filter(
|
const filteredAttributeKeys = searchedAttributeKeys.filter(
|
||||||
(item) => item.key !== 'body',
|
(item) => item.key !== 'body',
|
||||||
);
|
);
|
||||||
|
|
||||||
return getOptionsFromKeys(filteredAttributeKeys, selectedColumnKeys);
|
return getOptionsFromKeys(filteredAttributeKeys, selectedColumnKeys);
|
||||||
}, [attributeKeys, selectedColumnKeys]);
|
}, [searchedAttributeKeys, selectedColumnKeys]);
|
||||||
|
|
||||||
const handleRedirectWithOptionsData = useCallback(
|
const handleRedirectWithOptionsData = useCallback(
|
||||||
(newQueryData: OptionsQuery) => {
|
(newQueryData: OptionsQuery) => {
|
||||||
@ -90,13 +162,14 @@ const useOptionsMenu = ({
|
|||||||
[redirectWithOptionsData],
|
[redirectWithOptionsData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectedColumnsChange = useCallback(
|
const handleSelectColumns = useCallback(
|
||||||
(value: string[]) => {
|
(value: string) => {
|
||||||
const newSelectedColumnKeys = [
|
const newSelectedColumnKeys = [...new Set([...selectedColumnKeys, value])];
|
||||||
...new Set([...selectedColumnKeys, ...value]),
|
|
||||||
];
|
|
||||||
const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => {
|
const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => {
|
||||||
const column = attributeKeys.find(({ id }) => id === key);
|
const column = [
|
||||||
|
...searchedAttributeKeys,
|
||||||
|
...optionsQueryData.selectColumns,
|
||||||
|
].find(({ id }) => id === key);
|
||||||
|
|
||||||
if (!column) return acc;
|
if (!column) return acc;
|
||||||
return [...acc, column];
|
return [...acc, column];
|
||||||
@ -110,10 +183,10 @@ const useOptionsMenu = ({
|
|||||||
handleRedirectWithOptionsData(optionsData);
|
handleRedirectWithOptionsData(optionsData);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
searchedAttributeKeys,
|
||||||
selectedColumnKeys,
|
selectedColumnKeys,
|
||||||
optionsQueryData,
|
optionsQueryData,
|
||||||
handleRedirectWithOptionsData,
|
handleRedirectWithOptionsData,
|
||||||
attributeKeys,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -123,14 +196,20 @@ const useOptionsMenu = ({
|
|||||||
({ id }) => id !== columnKey,
|
({ id }) => id !== columnKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
const optionsData: OptionsQuery = {
|
if (!newSelectedColumns.length && dataSource !== DataSource.LOGS) {
|
||||||
...optionsQueryData,
|
notifications.error({
|
||||||
selectColumns: newSelectedColumns,
|
message: 'There must be at least one selected column',
|
||||||
};
|
});
|
||||||
|
} else {
|
||||||
|
const optionsData: OptionsQuery = {
|
||||||
|
...optionsQueryData,
|
||||||
|
selectColumns: newSelectedColumns,
|
||||||
|
};
|
||||||
|
|
||||||
handleRedirectWithOptionsData(optionsData);
|
handleRedirectWithOptionsData(optionsData);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[optionsQueryData, handleRedirectWithOptionsData],
|
[dataSource, notifications, optionsQueryData, handleRedirectWithOptionsData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFormatChange = useCallback(
|
const handleFormatChange = useCallback(
|
||||||
@ -157,13 +236,30 @@ const useOptionsMenu = ({
|
|||||||
[handleRedirectWithOptionsData, optionsQueryData],
|
[handleRedirectWithOptionsData, optionsQueryData],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSearchAttribute = useCallback((value: string) => {
|
||||||
|
setSearchText(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFocus = (): void => {
|
||||||
|
setIsFocused(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (): void => {
|
||||||
|
setIsFocused(false);
|
||||||
|
setSearchText('');
|
||||||
|
};
|
||||||
|
|
||||||
const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo(
|
const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
addColumn: {
|
addColumn: {
|
||||||
value: optionsQueryData.selectColumns || defaultOptionsQuery.selectColumns,
|
isFetching: isSearchedAttributesFetching,
|
||||||
options: addColumnOptions || [],
|
value: optionsQueryData?.selectColumns || defaultOptionsQuery.selectColumns,
|
||||||
onChange: handleSelectedColumnsChange,
|
options: optionsFromAttributeKeys || [],
|
||||||
|
onFocus: handleFocus,
|
||||||
|
onBlur: handleBlur,
|
||||||
|
onSelect: handleSelectColumns,
|
||||||
onRemove: handleRemoveSelectedColumn,
|
onRemove: handleRemoveSelectedColumn,
|
||||||
|
onSearch: handleSearchAttribute,
|
||||||
},
|
},
|
||||||
format: {
|
format: {
|
||||||
value: optionsQueryData.format || defaultOptionsQuery.format,
|
value: optionsQueryData.format || defaultOptionsQuery.format,
|
||||||
@ -175,11 +271,13 @@ const useOptionsMenu = ({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
addColumnOptions,
|
optionsFromAttributeKeys,
|
||||||
optionsQueryData?.maxLines,
|
optionsQueryData?.maxLines,
|
||||||
optionsQueryData?.format,
|
optionsQueryData?.format,
|
||||||
optionsQueryData?.selectColumns,
|
optionsQueryData?.selectColumns,
|
||||||
handleSelectedColumnsChange,
|
isSearchedAttributesFetching,
|
||||||
|
handleSearchAttribute,
|
||||||
|
handleSelectColumns,
|
||||||
handleRemoveSelectedColumn,
|
handleRemoveSelectedColumn,
|
||||||
handleFormatChange,
|
handleFormatChange,
|
||||||
handleMaxLinesChange,
|
handleMaxLinesChange,
|
||||||
@ -187,7 +285,7 @@ const useOptionsMenu = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (optionsQuery || !isFetched) return;
|
if (optionsQuery || !isFetchedInitialAttributes) return;
|
||||||
|
|
||||||
const nextOptionsQuery = localStorageOptionsQuery
|
const nextOptionsQuery = localStorageOptionsQuery
|
||||||
? JSON.parse(localStorageOptionsQuery)
|
? JSON.parse(localStorageOptionsQuery)
|
||||||
@ -195,15 +293,14 @@ const useOptionsMenu = ({
|
|||||||
|
|
||||||
redirectWithOptionsData(nextOptionsQuery);
|
redirectWithOptionsData(nextOptionsQuery);
|
||||||
}, [
|
}, [
|
||||||
isFetched,
|
isFetchedInitialAttributes,
|
||||||
optionsQuery,
|
optionsQuery,
|
||||||
initialOptionsQuery,
|
initialOptionsQuery,
|
||||||
redirectWithOptionsData,
|
|
||||||
localStorageOptionsQuery,
|
localStorageOptionsQuery,
|
||||||
|
redirectWithOptionsData,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading,
|
|
||||||
options: optionsQueryData,
|
options: optionsQueryData,
|
||||||
config: optionsMenuConfig,
|
config: optionsMenuConfig,
|
||||||
};
|
};
|
||||||
|
@ -14,15 +14,3 @@ export const getOptionsFromKeys = (
|
|||||||
({ value }) => !selectedKeys.find((key) => key === value),
|
({ value }) => !selectedKeys.find((key) => key === value),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getInitialColumns = (
|
|
||||||
initialColumnTitles: string[],
|
|
||||||
attributeKeys: BaseAutocompleteData[],
|
|
||||||
): BaseAutocompleteData[] =>
|
|
||||||
initialColumnTitles.reduce((acc, title) => {
|
|
||||||
const initialColumn = attributeKeys.find(({ key }) => title === key);
|
|
||||||
|
|
||||||
if (!initialColumn) return acc;
|
|
||||||
|
|
||||||
return [...acc, initialColumn];
|
|
||||||
}, [] as BaseAutocompleteData[]);
|
|
||||||
|
@ -65,7 +65,9 @@ function ListView(): JSX.Element {
|
|||||||
options?.selectColumns,
|
options?.selectColumns,
|
||||||
],
|
],
|
||||||
enabled:
|
enabled:
|
||||||
!!stagedQuery && panelType === PANEL_TYPES.LIST && !!options?.selectColumns,
|
!!stagedQuery &&
|
||||||
|
panelType === PANEL_TYPES.LIST &&
|
||||||
|
!!options?.selectColumns?.length,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user