From 2b28c5f2e272e7ec405809f0c02fed617254f5f5 Mon Sep 17 00:00:00 2001 From: Amlan Kumar Nandy <45410599+amlannandy@users.noreply.github.com> Date: Mon, 19 May 2025 11:54:05 +0700 Subject: [PATCH] chore: persist the filters and time selection, modal open state in summary view (#7942) --- .../InfraMonitoringK8s/commonUtils.tsx | 4 +- .../Summary/MetricNameSearch.tsx | 24 ++- .../Summary/MetricTypeSearch.tsx | 46 +++++- .../MetricsExplorer/Summary/MetricsSearch.tsx | 30 ++-- .../Summary/MetricsTreemap.tsx | 30 ++-- .../Summary/Summary.styles.scss | 16 +- .../MetricsExplorer/Summary/Summary.tsx | 76 +++++++-- .../Summary/__tests__/MetricsTable.test.tsx | 17 ++ .../Summary/__tests__/MetricsTreemap.test.tsx | 4 + .../Summary/__tests__/Summary.test.tsx | 150 ++++++++++++++++++ .../MetricsExplorer/Summary/constants.ts | 5 + .../MetricsExplorer/Summary/types.ts | 3 +- 12 files changed, 352 insertions(+), 53 deletions(-) create mode 100644 frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx diff --git a/frontend/src/container/InfraMonitoringK8s/commonUtils.tsx b/frontend/src/container/InfraMonitoringK8s/commonUtils.tsx index 37b5452251..d7f9fd9b79 100644 --- a/frontend/src/container/InfraMonitoringK8s/commonUtils.tsx +++ b/frontend/src/container/InfraMonitoringK8s/commonUtils.tsx @@ -5,9 +5,9 @@ /* eslint-disable prefer-destructuring */ import { Color } from '@signozhq/design-tokens'; -import { Tooltip, Typography } from 'antd'; -import Table, { ColumnsType } from 'antd/es/table'; +import { Table, Tooltip, Typography } from 'antd'; import { Progress } from 'antd/lib'; +import { ColumnsType } from 'antd/lib/table'; import { ResizeTable } from 'components/ResizeTable'; import FieldRenderer from 'container/LogDetailedView/FieldRenderer'; import { DataType } from 'container/LogDetailedView/TableView'; diff --git a/frontend/src/container/MetricsExplorer/Summary/MetricNameSearch.tsx b/frontend/src/container/MetricsExplorer/Summary/MetricNameSearch.tsx index b28db69413..b36d9c83d9 100644 --- a/frontend/src/container/MetricsExplorer/Summary/MetricNameSearch.tsx +++ b/frontend/src/container/MetricsExplorer/Summary/MetricNameSearch.tsx @@ -15,8 +15,11 @@ 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 { useSearchParams } from 'react-router-dom-v5-compat'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { COMPOSITE_QUERY_KEY } from './constants'; + function MetricNameSearch(): JSX.Element { const { currentQuery } = useQueryBuilder(); const { handleChangeQueryData } = useQueryOperations({ @@ -24,6 +27,7 @@ function MetricNameSearch(): JSX.Element { query: currentQuery.builder.queryData[0], entityVersion: '', }); + const [, setSearchParams] = useSearchParams(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [searchString, setSearchString] = useState(''); @@ -66,7 +70,7 @@ function MetricNameSearch(): JSX.Element { const handleSelect = useCallback( (selectedMetricName: string): void => { - handleChangeQueryData('filters', { + const newFilter = { items: [ ...currentQuery.builder.queryData[0].filters.items, { @@ -81,10 +85,26 @@ function MetricNameSearch(): JSX.Element { }, ], op: 'AND', + }; + const compositeQuery = { + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: [ + { + ...currentQuery.builder.queryData[0], + filters: newFilter, + }, + ], + }, + }; + handleChangeQueryData('filters', newFilter); + setSearchParams({ + [COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery), }); setIsPopoverOpen(false); }, - [currentQuery.builder.queryData, handleChangeQueryData], + [currentQuery, handleChangeQueryData, setSearchParams], ); const metricNameFilterValues = useMemo( diff --git a/frontend/src/container/MetricsExplorer/Summary/MetricTypeSearch.tsx b/frontend/src/container/MetricsExplorer/Summary/MetricTypeSearch.tsx index 24451c9ecf..462b110e90 100644 --- a/frontend/src/container/MetricsExplorer/Summary/MetricTypeSearch.tsx +++ b/frontend/src/container/MetricsExplorer/Summary/MetricTypeSearch.tsx @@ -4,8 +4,13 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { Search } from 'lucide-react'; import { useCallback, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom-v5-compat'; -import { METRIC_TYPE_LABEL_MAP, METRIC_TYPE_VALUES_MAP } from './constants'; +import { + COMPOSITE_QUERY_KEY, + METRIC_TYPE_LABEL_MAP, + METRIC_TYPE_VALUES_MAP, +} from './constants'; function MetricTypeSearch(): JSX.Element { const { currentQuery } = useQueryBuilder(); @@ -15,6 +20,7 @@ function MetricTypeSearch(): JSX.Element { entityVersion: '', }); + const [, setSearchParams] = useSearchParams(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const menuItems = useMemo( @@ -34,7 +40,7 @@ function MetricTypeSearch(): JSX.Element { const handleSelect = useCallback( (selectedMetricType: string): void => { if (selectedMetricType !== 'all') { - handleChangeQueryData('filters', { + const newFilter = { items: [ ...currentQuery.builder.queryData[0].filters.items, { @@ -49,18 +55,50 @@ function MetricTypeSearch(): JSX.Element { }, ], op: 'AND', + }; + const compositeQuery = { + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: [ + { + ...currentQuery.builder.queryData[0], + filters: newFilter, + }, + ], + }, + }; + handleChangeQueryData('filters', newFilter); + setSearchParams({ + [COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery), }); } else { - handleChangeQueryData('filters', { + const newFilter = { items: currentQuery.builder.queryData[0].filters.items.filter( (item) => item.id !== 'metric_type', ), op: 'AND', + }; + const compositeQuery = { + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: [ + { + ...currentQuery.builder.queryData[0], + filters: newFilter, + }, + ], + }, + }; + handleChangeQueryData('filters', newFilter); + setSearchParams({ + [COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery), }); } setIsPopoverOpen(false); }, - [currentQuery.builder.queryData, handleChangeQueryData], + [currentQuery, handleChangeQueryData, setSearchParams], ); const menu = ( diff --git a/frontend/src/container/MetricsExplorer/Summary/MetricsSearch.tsx b/frontend/src/container/MetricsExplorer/Summary/MetricsSearch.tsx index e004191195..ca0a3f4aad 100644 --- a/frontend/src/container/MetricsExplorer/Summary/MetricsSearch.tsx +++ b/frontend/src/container/MetricsExplorer/Summary/MetricsSearch.tsx @@ -1,32 +1,13 @@ -import { Select, Tooltip } from 'antd'; +import { Tooltip } from 'antd'; import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import { HardHat, Info } from 'lucide-react'; -import { TREEMAP_VIEW_OPTIONS } from './constants'; import { MetricsSearchProps } from './types'; -function MetricsSearch({ - query, - onChange, - heatmapView, - setHeatmapView, -}: MetricsSearchProps): JSX.Element { +function MetricsSearch({ query, onChange }: MetricsSearchProps): JSX.Element { return (
-
-
( TreemapViewType.TIMESERIES, ); - const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(false); - const [isInspectModalOpen, setIsInspectModalOpen] = useState(false); - const [selectedMetricName, setSelectedMetricName] = useState( - null, + + const [searchParams, setSearchParams] = useSearchParams(); + const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState( + () => searchParams.get(IS_METRIC_DETAILS_OPEN_KEY) === 'true' || false, + ); + const [isInspectModalOpen, setIsInspectModalOpen] = useState( + () => searchParams.get(IS_INSPECT_MODAL_OPEN_KEY) === 'true' || false, + ); + const [selectedMetricName, setSelectedMetricName] = useState( + () => searchParams.get(SELECTED_METRIC_NAME_KEY) || null, ); const { maxTime, minTime } = useSelector( @@ -75,13 +88,25 @@ function Summary(): JSX.Element { useShareBuilderUrl(defaultQuery); + // This is used to avoid the filters from being serialized with the id + const currentQueryFiltersString = useMemo(() => { + const filters = currentQuery?.builder?.queryData[0]?.filters; + if (!filters) return ''; + const filtersWithoutId = { + ...filters, + items: filters.items.map(({ id, ...rest }) => rest), + }; + return JSON.stringify(filtersWithoutId); + }, [currentQuery]); + const queryFilters = useMemo( () => currentQuery?.builder?.queryData[0]?.filters || { items: [], op: 'and', }, - [currentQuery], + // eslint-disable-next-line react-hooks/exhaustive-deps + [currentQueryFiltersString], ); const { handleChangeQueryData } = useQueryOperations({ @@ -145,9 +170,24 @@ function Summary(): JSX.Element { const handleFilterChange = useCallback( (value: TagFilter) => { handleChangeQueryData('filters', value); + const compositeQuery = { + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: [ + { + ...currentQuery.builder.queryData[0], + filters: value, + }, + ], + }, + }; + setSearchParams({ + [COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery), + }); setCurrentPage(1); }, - [handleChangeQueryData], + [handleChangeQueryData, currentQuery, setSearchParams], ); const updatedCurrentQuery = useMemo( @@ -184,17 +224,29 @@ function Summary(): JSX.Element { const openMetricDetails = (metricName: string): void => { setSelectedMetricName(metricName); setIsMetricDetailsOpen(true); + setSearchParams({ + [IS_METRIC_DETAILS_OPEN_KEY]: 'true', + [SELECTED_METRIC_NAME_KEY]: metricName, + }); }; const closeMetricDetails = (): void => { setSelectedMetricName(null); setIsMetricDetailsOpen(false); + setSearchParams({ + [IS_METRIC_DETAILS_OPEN_KEY]: 'false', + [SELECTED_METRIC_NAME_KEY]: '', + }); }; const openInspectModal = (metricName: string): void => { setSelectedMetricName(metricName); setIsInspectModalOpen(true); setIsMetricDetailsOpen(false); + setSearchParams({ + [IS_INSPECT_MODAL_OPEN_KEY]: 'true', + [SELECTED_METRIC_NAME_KEY]: metricName, + }); }; const closeInspectModal = (): void => { @@ -204,23 +256,23 @@ function Summary(): JSX.Element { }); setIsInspectModalOpen(false); setSelectedMetricName(null); + setSearchParams({ + [IS_INSPECT_MODAL_OPEN_KEY]: 'false', + [SELECTED_METRIC_NAME_KEY]: '', + }); }; return ( }>
- + { + const actual = jest.requireActual('react-router-dom-v5-compat'); + return { + ...actual, + useSearchParams: jest.fn().mockReturnValue([{}, jest.fn()]), + useNavigationType: (): any => 'PUSH', + }; +}); describe('MetricsTable', () => { + beforeEach(() => { + jest + .spyOn(useQueryBuilderOperationsHooks, 'useQueryOperations') + .mockReturnValue({ + handleChangeQueryData: jest.fn(), + } as any); + }); + jest .spyOn(useGetMetricsListFilterValues, 'useGetMetricsListFilterValues') .mockReturnValue({ diff --git a/frontend/src/container/MetricsExplorer/Summary/__tests__/MetricsTreemap.test.tsx b/frontend/src/container/MetricsExplorer/Summary/__tests__/MetricsTreemap.test.tsx index 78465e4e19..ab4c885c56 100644 --- a/frontend/src/container/MetricsExplorer/Summary/__tests__/MetricsTreemap.test.tsx +++ b/frontend/src/container/MetricsExplorer/Summary/__tests__/MetricsTreemap.test.tsx @@ -55,6 +55,7 @@ describe('MetricsTreemap', () => { }} openMetricDetails={jest.fn()} viewType={TreemapViewType.SAMPLES} + setHeatmapView={jest.fn()} /> , @@ -79,6 +80,7 @@ describe('MetricsTreemap', () => { }} openMetricDetails={jest.fn()} viewType={TreemapViewType.SAMPLES} + setHeatmapView={jest.fn()} /> , @@ -105,6 +107,7 @@ describe('MetricsTreemap', () => { }} openMetricDetails={jest.fn()} viewType={TreemapViewType.SAMPLES} + setHeatmapView={jest.fn()} /> , @@ -128,6 +131,7 @@ describe('MetricsTreemap', () => { data={null} openMetricDetails={jest.fn()} viewType={TreemapViewType.SAMPLES} + setHeatmapView={jest.fn()} /> , diff --git a/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx b/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx new file mode 100644 index 0000000000..276111c77c --- /dev/null +++ b/frontend/src/container/MetricsExplorer/Summary/__tests__/Summary.test.tsx @@ -0,0 +1,150 @@ +import { render, screen } from '@testing-library/react'; +import { MetricType } from 'api/metricsExplorer/getMetricsList'; +import ROUTES from 'constants/routes'; +import * as useGetMetricsListHooks from 'hooks/metricsExplorer/useGetMetricsList'; +import * as useGetMetricsTreeMapHooks from 'hooks/metricsExplorer/useGetMetricsTreeMap'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { Provider } from 'react-redux'; +import { useSearchParams } from 'react-router-dom-v5-compat'; +import store from 'store'; + +import Summary from '../Summary'; +import { TreemapViewType } from '../types'; + +jest.mock('uplot', () => { + const paths = { + spline: jest.fn(), + bars: jest.fn(), + }; + const uplotMock = jest.fn(() => ({ + paths, + })); + return { + paths, + default: uplotMock, + }; +}); +jest.mock('d3-hierarchy', () => ({ + stratify: jest.fn().mockReturnValue({ + id: jest.fn().mockReturnValue({ + parentId: jest.fn().mockReturnValue( + jest.fn().mockReturnValue({ + sum: jest.fn().mockReturnValue({ + descendants: jest.fn().mockReturnValue([]), + eachBefore: jest.fn().mockReturnValue([]), + }), + }), + ), + }), + }), + treemapBinary: jest.fn(), +})); +jest.mock('react-use', () => ({ + useWindowSize: jest.fn().mockReturnValue({ width: 1000, height: 1000 }), +})); +jest.mock('react-router-dom-v5-compat', () => { + const actual = jest.requireActual('react-router-dom-v5-compat'); + return { + ...actual, + useSearchParams: jest.fn(), + useNavigationType: (): any => 'PUSH', + }; +}); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: (): { pathname: string } => ({ + pathname: `${ROUTES.METRICS_EXPLORER_BASE}`, + }), +})); + +const queryClient = new QueryClient(); +const mockMetricName = 'test-metric'; +jest.spyOn(useGetMetricsListHooks, 'useGetMetricsList').mockReturnValue({ + data: { + payload: { + status: 'success', + data: { + metrics: [ + { + metric_name: mockMetricName, + description: 'description for a test metric', + type: MetricType.GAUGE, + unit: 'count', + lastReceived: '1715702400', + [TreemapViewType.TIMESERIES]: 100, + [TreemapViewType.SAMPLES]: 100, + }, + ], + }, + }, + }, + isError: false, + isLoading: false, +} as any); +jest.spyOn(useGetMetricsTreeMapHooks, 'useGetMetricsTreeMap').mockReturnValue({ + data: { + payload: { + status: 'success', + data: { + [TreemapViewType.TIMESERIES]: [ + { + metric_name: mockMetricName, + percentage: 100, + total_value: 100, + }, + ], + [TreemapViewType.SAMPLES]: [ + { + metric_name: mockMetricName, + percentage: 100, + }, + ], + }, + }, + }, + isError: false, + isLoading: false, +} as any); +const mockSetSearchParams = jest.fn(); + +describe('Summary', () => { + it('persists inspect modal open state across page refresh', () => { + (useSearchParams as jest.Mock).mockReturnValue([ + new URLSearchParams({ + isInspectModalOpen: 'true', + selectedMetricName: 'test-metric', + }), + mockSetSearchParams, + ]); + + render( + + + + + , + ); + + expect(screen.queryByText('Proportion View')).not.toBeInTheDocument(); + }); + + it('persists metric details modal state across page refresh', () => { + (useSearchParams as jest.Mock).mockReturnValue([ + new URLSearchParams({ + isMetricDetailsOpen: 'true', + selectedMetricName: mockMetricName, + }), + mockSetSearchParams, + ]); + + render( + + + + + , + ); + + expect(screen.queryByText('Proportion View')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/container/MetricsExplorer/Summary/constants.ts b/frontend/src/container/MetricsExplorer/Summary/constants.ts index 0b782d9268..69d46900bc 100644 --- a/frontend/src/container/MetricsExplorer/Summary/constants.ts +++ b/frontend/src/container/MetricsExplorer/Summary/constants.ts @@ -32,3 +32,8 @@ export const METRIC_TYPE_VALUES_MAP = { [MetricType.SUMMARY]: 'Summary', [MetricType.EXPONENTIAL_HISTOGRAM]: 'ExponentialHistogram', }; + +export const IS_METRIC_DETAILS_OPEN_KEY = 'isMetricDetailsOpen'; +export const IS_INSPECT_MODAL_OPEN_KEY = 'isInspectModalOpen'; +export const SELECTED_METRIC_NAME_KEY = 'selectedMetricName'; +export const COMPOSITE_QUERY_KEY = 'compositeQuery'; diff --git a/frontend/src/container/MetricsExplorer/Summary/types.ts b/frontend/src/container/MetricsExplorer/Summary/types.ts index f1a30585b2..a5febe0823 100644 --- a/frontend/src/container/MetricsExplorer/Summary/types.ts +++ b/frontend/src/container/MetricsExplorer/Summary/types.ts @@ -20,8 +20,6 @@ export interface MetricsTableProps { export interface MetricsSearchProps { query: IBuilderQuery; onChange: (value: TagFilter) => void; - heatmapView: TreemapViewType; - setHeatmapView: (value: TreemapViewType) => void; } export interface MetricsTreemapProps { @@ -30,6 +28,7 @@ export interface MetricsTreemapProps { isError: boolean; viewType: TreemapViewType; openMetricDetails: (metricName: string) => void; + setHeatmapView: (value: TreemapViewType) => void; } export interface OrderByPayload {