mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 06:35:56 +08:00
chore: add in-table-search for metric name and type in metrics explorer (#7356)
This commit is contained in:
parent
a64908e571
commit
097e4ca948
@ -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[];
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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<boolean>(false);
|
||||
const [searchString, setSearchString] = useState<string>('');
|
||||
const [debouncedSearchString, setDebouncedSearchString] = useState<string>('');
|
||||
const [highlightedIndex, setHighlightedIndex] = useState<number>(-1);
|
||||
const menuRef = useRef<MenuRef | null>(null);
|
||||
const inputRef = useRef<InputRef | null>(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<HTMLInputElement>) => {
|
||||
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(
|
||||
<Menu.Item
|
||||
key={searchString}
|
||||
onClick={(): void => handleSelect(searchString)}
|
||||
className={highlightedIndex === 0 ? 'highlighted' : ''}
|
||||
>
|
||||
{searchString}
|
||||
</Menu.Item>,
|
||||
);
|
||||
}
|
||||
if (isLoadingMetricNameFilterValues) {
|
||||
items.push(<Spin />);
|
||||
} else if (isErrorMetricNameFilterValues) {
|
||||
items.push(<Empty description="Error fetching metric names" />);
|
||||
} else if (metricNameFilterValues?.length === 0) {
|
||||
items.push(<Empty description="No metric names found" />);
|
||||
} else {
|
||||
items.push(
|
||||
...metricNameFilterValues.map((filterValue, index) => (
|
||||
<Menu.Item
|
||||
key={filterValue}
|
||||
onClick={(): void => handleSelect(filterValue)}
|
||||
className={highlightedIndex === index ? 'highlighted' : ''}
|
||||
>
|
||||
{filterValue}
|
||||
</Menu.Item>
|
||||
)),
|
||||
);
|
||||
}
|
||||
return items;
|
||||
}, [
|
||||
handleSelect,
|
||||
highlightedIndex,
|
||||
isErrorMetricNameFilterValues,
|
||||
isLoadingMetricNameFilterValues,
|
||||
metricNameFilterValues,
|
||||
searchString,
|
||||
]);
|
||||
|
||||
const debouncedUpdate = useDebouncedFn((value) => {
|
||||
setDebouncedSearchString(value as string);
|
||||
}, 400);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = e.target.value.trim().toLowerCase();
|
||||
setSearchString(value);
|
||||
debouncedUpdate(value);
|
||||
},
|
||||
[debouncedUpdate],
|
||||
);
|
||||
|
||||
const popoverContent = useMemo(
|
||||
() => (
|
||||
<div className="metric-name-search-popover">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search..."
|
||||
value={searchString}
|
||||
onChange={handleInputChange}
|
||||
bordered
|
||||
/>
|
||||
<Menu ref={menuRef} className="metric-name-search-popover-menu">
|
||||
{popoverItems}
|
||||
</Menu>
|
||||
</div>
|
||||
),
|
||||
[handleKeyDown, searchString, handleInputChange, popoverItems],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPopoverOpen) {
|
||||
setSearchString('');
|
||||
setDebouncedSearchString('');
|
||||
}
|
||||
}, [isPopoverOpen]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={popoverContent}
|
||||
trigger="click"
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={(val): void => setIsPopoverOpen(val)}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<Search size={14} />}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricNameSearch;
|
@ -0,0 +1,92 @@
|
||||
import { Button, Menu, Popover, Tooltip } from 'antd';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Search } from 'lucide-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { METRIC_TYPE_LABEL_MAP, METRIC_TYPE_VALUES_MAP } from './constants';
|
||||
|
||||
function MetricTypeSearch(): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'all',
|
||||
value: 'All',
|
||||
},
|
||||
...Object.keys(METRIC_TYPE_LABEL_MAP).map((key) => ({
|
||||
key: METRIC_TYPE_VALUES_MAP[key as MetricType],
|
||||
value: METRIC_TYPE_LABEL_MAP[key as MetricType],
|
||||
})),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(selectedMetricType: string): void => {
|
||||
if (selectedMetricType !== 'all') {
|
||||
handleChangeQueryData('filters', {
|
||||
items: [
|
||||
{
|
||||
id: 'metric_type',
|
||||
op: '=',
|
||||
key: {
|
||||
id: 'metric_type',
|
||||
key: 'metric_type',
|
||||
type: 'tag',
|
||||
},
|
||||
value: selectedMetricType,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
});
|
||||
} else {
|
||||
handleChangeQueryData('filters', {
|
||||
items: currentQuery.builder.queryData[0].filters.items.filter(
|
||||
(item) => item.id !== 'metric_type',
|
||||
),
|
||||
op: 'AND',
|
||||
});
|
||||
}
|
||||
setIsPopoverOpen(false);
|
||||
},
|
||||
[currentQuery.builder.queryData, handleChangeQueryData],
|
||||
);
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
{menuItems.map((menuItem) => (
|
||||
<Menu.Item
|
||||
key={menuItem.key}
|
||||
onClick={(): void => handleSelect(menuItem.key)}
|
||||
>
|
||||
{menuItem.value}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={menu}
|
||||
trigger="click"
|
||||
open={isPopoverOpen}
|
||||
onOpenChange={(val): void => setIsPopoverOpen(val)}
|
||||
>
|
||||
<Tooltip title="Filter by metric type">
|
||||
<Button type="text" shape="circle" icon={<Search size={14} />} />
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricTypeSearch;
|
@ -80,6 +80,22 @@
|
||||
background: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.metric-name-column-header,
|
||||
.metric-type-column-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.metric-name-column-header-text,
|
||||
.metric-type-column-header-text {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
margin-left: 4px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
@ -263,3 +279,26 @@
|
||||
padding: 5px 10px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.ant-popover-inner {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.metric-name-search-popover {
|
||||
.metric-name-search-popover-menu {
|
||||
height: 200px;
|
||||
width: 300px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.ant-menu {
|
||||
.ant-spin,
|
||||
.ant-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,15 +20,21 @@ import {
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { METRIC_TYPE_LABEL_MAP } from './constants';
|
||||
import MetricNameSearch from './MetricNameSearch';
|
||||
import MetricTypeSearch from './MetricTypeSearch';
|
||||
import { MetricsListItemRowData, TreemapTile, TreemapViewType } from './types';
|
||||
|
||||
export const metricsTableColumns: ColumnType<MetricsListItemRowData>[] = [
|
||||
{
|
||||
title: <div className="metric-name-column-header">METRIC</div>,
|
||||
title: (
|
||||
<div className="metric-name-column-header">
|
||||
<span className="metric-name-column-header-text">METRIC</span>
|
||||
<MetricNameSearch />
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'metric_name',
|
||||
width: 400,
|
||||
sorter: true,
|
||||
className: 'metric-name-column-header',
|
||||
sorter: false,
|
||||
render: (value: string): React.ReactNode => (
|
||||
<div className="metric-name-column-value">{value}</div>
|
||||
),
|
||||
@ -44,9 +50,14 @@ export const metricsTableColumns: ColumnType<MetricsListItemRowData>[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'TYPE',
|
||||
title: (
|
||||
<div className="metric-type-column-header">
|
||||
<span className="metric-type-column-header-text">TYPE</span>
|
||||
<MetricTypeSearch />
|
||||
</div>
|
||||
),
|
||||
dataIndex: 'metric_type',
|
||||
sorter: true,
|
||||
sorter: false,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
|
@ -0,0 +1,42 @@
|
||||
import {
|
||||
getMetricsListFilterValues,
|
||||
MetricsListFilterValuesPayload,
|
||||
MetricsListFilterValuesResponse,
|
||||
} from 'api/metricsExplorer/getMetricsListFilterValues';
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
type UseGetMetricsListFilterValues = (
|
||||
payload: MetricsListFilterValuesPayload,
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse,
|
||||
Error
|
||||
>,
|
||||
headers?: Record<string, string>,
|
||||
) => UseQueryResult<
|
||||
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse,
|
||||
Error
|
||||
>;
|
||||
|
||||
export const useGetMetricsListFilterValues: UseGetMetricsListFilterValues = (
|
||||
props,
|
||||
options,
|
||||
headers,
|
||||
) => {
|
||||
const queryKey = useMemo(() => {
|
||||
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||
return [...options.queryKey];
|
||||
}
|
||||
return [props];
|
||||
}, [options?.queryKey, props]);
|
||||
|
||||
return useQuery<
|
||||
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse,
|
||||
Error
|
||||
>({
|
||||
queryFn: ({ signal }) => getMetricsListFilterValues(props, signal, headers),
|
||||
...options,
|
||||
queryKey,
|
||||
});
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user