diff --git a/frontend/src/api/metricsExplorer/getMetricsListFilterValues.ts b/frontend/src/api/metricsExplorer/getMetricsListFilterValues.ts index addfc91750..6b4cd6d317 100644 --- a/frontend/src/api/metricsExplorer/getMetricsListFilterValues.ts +++ b/frontend/src/api/metricsExplorer/getMetricsListFilterValues.ts @@ -2,7 +2,6 @@ import axios from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; import { ErrorResponse, SuccessResponse } from 'types/api'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; export interface MetricsListFilterValuesPayload { filterAttributeKeyDataType: string; @@ -14,7 +13,7 @@ export interface MetricsListFilterValuesPayload { export interface MetricsListFilterValuesResponse { status: string; data: { - FilterValues: BaseAutocompleteData[]; + filterValues: string[]; }; } diff --git a/frontend/src/container/MetricsExplorer/Summary/MetricNameSearch.tsx b/frontend/src/container/MetricsExplorer/Summary/MetricNameSearch.tsx new file mode 100644 index 0000000000..4de3cc067b --- /dev/null +++ b/frontend/src/container/MetricsExplorer/Summary/MetricNameSearch.tsx @@ -0,0 +1,232 @@ +import { + Button, + Empty, + Input, + InputRef, + Menu, + MenuRef, + Popover, + Spin, +} from 'antd'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useGetMetricsListFilterValues } from 'hooks/metricsExplorer/useGetMetricsListFilterValues'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; +import useDebouncedFn from 'hooks/useDebouncedFunction'; +import { Search } from 'lucide-react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +function MetricNameSearch(): JSX.Element { + const { currentQuery } = useQueryBuilder(); + const { handleChangeQueryData } = useQueryOperations({ + index: 0, + query: currentQuery.builder.queryData[0], + entityVersion: '', + }); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [searchString, setSearchString] = useState(''); + const [debouncedSearchString, setDebouncedSearchString] = useState(''); + const [highlightedIndex, setHighlightedIndex] = useState(-1); + const menuRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + if (isPopoverOpen) { + setTimeout(() => { + inputRef.current?.focus(); + }, 0); // Ensures focus happens after popover opens + setHighlightedIndex(-1); + } + }, [isPopoverOpen]); + + const { + data: metricNameFilterValuesData, + isLoading: isLoadingMetricNameFilterValues, + isError: isErrorMetricNameFilterValues, + } = useGetMetricsListFilterValues( + { + searchText: debouncedSearchString, + filterKey: 'metric_name', + filterAttributeKeyDataType: DataTypes.String, + limit: 10, + }, + { + enabled: isPopoverOpen, + refetchOnWindowFocus: false, + queryKey: [ + REACT_QUERY_KEY.GET_METRICS_LIST_FILTER_VALUES, + 'metric_name', + debouncedSearchString, + isPopoverOpen, + ], + }, + ); + + const handleSelect = useCallback( + (selectedMetricName: string): void => { + handleChangeQueryData('filters', { + items: [ + { + id: 'metric_name', + op: 'CONTAINS', + key: { + id: 'metric_name', + key: 'metric_name', + type: 'tag', + }, + value: selectedMetricName, + }, + ], + op: 'AND', + }); + setIsPopoverOpen(false); + }, + [handleChangeQueryData], + ); + + const metricNameFilterValues = useMemo( + () => metricNameFilterValuesData?.payload?.data?.filterValues || [], + [metricNameFilterValuesData], + ); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (!isPopoverOpen) return; + + if (event.key === 'ArrowDown') { + event.preventDefault(); + setHighlightedIndex((prev) => { + const nextIndex = prev < metricNameFilterValues.length - 1 ? prev + 1 : 0; + menuRef.current?.focus(); + return nextIndex; + }); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + setHighlightedIndex((prev) => { + const prevIndex = prev > 0 ? prev - 1 : metricNameFilterValues.length - 1; + menuRef.current?.focus(); + return prevIndex; + }); + } else if (event.key === 'Enter') { + event.preventDefault(); + // If there is a highlighted item, select it + if (highlightedIndex >= 0 && metricNameFilterValues[highlightedIndex]) { + handleSelect(metricNameFilterValues[highlightedIndex]); + } else if (highlightedIndex === -1 && searchString) { + // If there is no highlighted item and there is a search string, select the search string + handleSelect(searchString); + } + } + }, + [ + isPopoverOpen, + highlightedIndex, + metricNameFilterValues, + searchString, + handleSelect, + ], + ); + + const popoverItems = useMemo(() => { + const items: JSX.Element[] = []; + if (searchString) { + items.push( + handleSelect(searchString)} + className={highlightedIndex === 0 ? 'highlighted' : ''} + > + {searchString} + , + ); + } + if (isLoadingMetricNameFilterValues) { + items.push(); + } else if (isErrorMetricNameFilterValues) { + items.push(); + } else if (metricNameFilterValues?.length === 0) { + items.push(); + } else { + items.push( + ...metricNameFilterValues.map((filterValue, index) => ( + handleSelect(filterValue)} + className={highlightedIndex === index ? 'highlighted' : ''} + > + {filterValue} + + )), + ); + } + return items; + }, [ + handleSelect, + highlightedIndex, + isErrorMetricNameFilterValues, + isLoadingMetricNameFilterValues, + metricNameFilterValues, + searchString, + ]); + + const debouncedUpdate = useDebouncedFn((value) => { + setDebouncedSearchString(value as string); + }, 400); + + const handleInputChange = useCallback( + (e: React.ChangeEvent): void => { + const value = e.target.value.trim().toLowerCase(); + setSearchString(value); + debouncedUpdate(value); + }, + [debouncedUpdate], + ); + + const popoverContent = useMemo( + () => ( +
+ + + {popoverItems} + +
+ ), + [handleKeyDown, searchString, handleInputChange, popoverItems], + ); + + useEffect(() => { + if (!isPopoverOpen) { + setSearchString(''); + setDebouncedSearchString(''); + } + }, [isPopoverOpen]); + + return ( + setIsPopoverOpen(val)} + > +