diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 1a397372730..dbcf596e2d5 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -10,7 +10,7 @@ import { useState, } from 'react'; // eslint-disable-next-line no-restricted-imports -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import getFromLocalstorage from 'api/browser/localstorage/get'; import setToLocalstorage from 'api/browser/localstorage/set'; import logEvent from 'api/common/logEvent'; @@ -42,7 +42,6 @@ import useUrlQueryData from 'hooks/useUrlQueryData'; import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit'; import { isEmpty, isUndefined } from 'lodash-es'; import LiveLogs from 'pages/LiveLogs'; -import { UpdateTimeInterval } from 'store/actions'; import { AppState } from 'store/reducers'; import { Warning } from 'types/api'; import { Dashboard } from 'types/api/dashboard/getAll'; @@ -77,7 +76,6 @@ function LogsExplorerViewsContainer({ handleChangeSelectedView: ChangeViewFunctionType; }): JSX.Element { const { safeNavigate } = useSafeNavigate(); - const dispatch = useDispatch(); const [showFrequencyChart, setShowFrequencyChart] = useState( () => getFromLocalstorage(LOCALSTORAGE.SHOW_FREQUENCY_CHART) === 'true', @@ -90,10 +88,9 @@ function LogsExplorerViewsContainer({ DEFAULT_PER_PAGE_VALUE, ); - const { minTime, maxTime, selectedTime } = useSelector< - AppState, - GlobalReducer - >((state) => state.globalTime); + const { minTime, maxTime } = useSelector( + (state) => state.globalTime, + ); const currentMinTimeRef = useRef(minTime); @@ -329,16 +326,6 @@ function LogsExplorerViewsContainer({ currentMinTimeRef.current !== minTime || orderByChanged ) { - // Recalculate global time when query changes i.e. stage and run query clicked - if ( - !!requestData?.id && - stagedQuery?.id && - requestData?.id !== stagedQuery?.id && - selectedTime !== 'custom' - ) { - dispatch(UpdateTimeInterval(selectedTime)); - } - const newRequestData = getRequestData(stagedQuery, { filters: listQuery?.filters || initialFilters, filter: listQuery?.filter || { expression: '' }, @@ -360,8 +347,6 @@ function LogsExplorerViewsContainer({ minTime, activeLogId, selectedPanelType, - dispatch, - selectedTime, maxTime, orderBy, ]); diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerPagination.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerPagination.test.tsx index 7f7974fc0aa..f4303769317 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerPagination.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerPagination.test.tsx @@ -108,13 +108,21 @@ jest.mock('hooks/useSafeNavigate', () => ({ }), })); -jest.mock( - 'container/TopNav/DateTimeSelectionV2/index.tsx', - () => - function MockDateTimeSelection(): JSX.Element { - return
MockDateTimeSelection
; - }, -); +jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => { + const { useQueryBuilder } = jest.requireActual( + 'hooks/queryBuilder/useQueryBuilder', + ); + const { useSyncTimeOnStagedQueryChange } = jest.requireActual( + 'hooks/queryBuilder/useSyncTimeOnStagedQueryChange', + ); + + return function MockDateTimeSelection(): JSX.Element { + const { stagedQuery } = useQueryBuilder(); + useSyncTimeOnStagedQueryChange(stagedQuery?.id); + return
MockDateTimeSelection
; + }; +}); + jest.mock( 'container/LogsExplorerChart', () => diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss index b42c9d88f17..9cd465a93fd 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss @@ -233,8 +233,10 @@ background: var(--l1-background); box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); padding: 0px; + gap: 0; + margin: 4px; - .ant-typography { + .qb-tag-text { color: var(--l1-foreground); font-family: Inter; font-size: 14px !important; @@ -244,7 +246,7 @@ padding: 2px 6px; } - .close-icon { + > button { display: flex; align-items: center; justify-content: center; @@ -259,26 +261,26 @@ &.resource { border: 1px solid color-mix(in srgb, var(--bg-aqua-400) 13%, transparent); - .ant-typography { + .qb-tag-text { color: var(--bg-aqua-400); background: color-mix(in srgb, var(--bg-aqua-400) 6%, transparent); font-size: 14px; } - .close-icon { + > button { background: color-mix(in srgb, var(--bg-aqua-400) 6%, transparent); } } &.tag { border: 1px solid color-mix(in srgb, var(--bg-sienna-400) 20%, transparent); - .ant-typography { + .qb-tag-text { color: var(--bg-sienna-400); background: color-mix(in srgb, var(--bg-sienna-400) 10%, transparent); font-size: 14px; } - .close-icon { + > button { background: color-mix(in srgb, var(--bg-sienna-400) 10%, transparent); } } @@ -286,13 +288,13 @@ &.scope { border: 1px solid color-mix(in srgb, var(--bg-robin-400) 20%, transparent); - .ant-typography { + .qb-tag-text { color: var(--bg-robin-400); background: color-mix(in srgb, var(--bg-robin-400) 10%, transparent); font-size: 14px; } - .close-icon { + > button { background: color-mix(in srgb, var(--bg-robin-400) 10%, transparent); } } diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx index c85f5b991f9..0b660a2ae92 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx @@ -966,6 +966,7 @@ function QueryBuilderSearchV2( > { diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx index 30ea89f3a65..5e34e17f8d2 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx @@ -19,6 +19,7 @@ import { useIsGlobalTimeQueryRefreshing, } from 'store/globalTime'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useSyncTimeOnStagedQueryChange } from 'hooks/queryBuilder/useSyncTimeOnStagedQueryChange'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { isValidShortHandDateTimeFormat } from 'lib/getMinMax'; import getTimeString from 'lib/getTimeString'; @@ -187,6 +188,8 @@ function DateTimeSelection({ const { stagedQuery, currentQuery, initQueryBuilderData } = useQueryBuilder(); + useSyncTimeOnStagedQueryChange(stagedQuery?.id); + const getInputLabel = ( startTime?: Dayjs, endTime?: Dayjs, diff --git a/frontend/src/hooks/queryBuilder/__tests__/useSyncTimeOnStagedQueryChange.test.ts b/frontend/src/hooks/queryBuilder/__tests__/useSyncTimeOnStagedQueryChange.test.ts new file mode 100644 index 00000000000..c0c0c723009 --- /dev/null +++ b/frontend/src/hooks/queryBuilder/__tests__/useSyncTimeOnStagedQueryChange.test.ts @@ -0,0 +1,139 @@ +import { renderHook } from '@testing-library/react'; +// eslint-disable-next-line no-restricted-imports +import { useSelector } from 'react-redux'; +import { UpdateTimeInterval } from 'store/actions'; + +import { useSyncTimeOnStagedQueryChange } from '../useSyncTimeOnStagedQueryChange'; + +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => ({ + useDispatch: (): jest.Mock => mockDispatch, + useSelector: jest.fn(), +})); + +jest.mock('store/actions', () => ({ + UpdateTimeInterval: jest.fn((time: string) => ({ + type: 'UPDATE_TIME_INTERVAL_THUNK', + payload: time, + })), +})); + +const mockedUseSelector = useSelector as jest.Mock; +const mockedUpdateTimeInterval = UpdateTimeInterval as unknown as jest.Mock; + +const setSelectedTime = (value: string): void => { + mockedUseSelector.mockImplementation( + (selector: (state: { globalTime: { selectedTime: string } }) => unknown) => + selector({ globalTime: { selectedTime: value } }), + ); +}; + +describe('useSyncTimeOnStagedQueryChange', () => { + beforeEach(() => { + jest.clearAllMocks(); + setSelectedTime('1h'); + }); + + it('does not dispatch on initial mount when stagedQueryId is undefined', () => { + renderHook(() => useSyncTimeOnStagedQueryChange(undefined)); + expect(mockDispatch).not.toHaveBeenCalled(); + }); + + it('does not dispatch on initial mount when stagedQueryId is already defined', () => { + renderHook(() => useSyncTimeOnStagedQueryChange('initial-id')); + expect(mockDispatch).not.toHaveBeenCalled(); + }); + + it('does not dispatch when stagedQueryId transitions from undefined to defined (first staged query arriving)', () => { + const { rerender } = renderHook( + ({ id }: { id: string | undefined }) => useSyncTimeOnStagedQueryChange(id), + { initialProps: { id: undefined as string | undefined } }, + ); + + rerender({ id: 'first-id' }); + + expect(mockDispatch).not.toHaveBeenCalled(); + }); + + it('dispatches UpdateTimeInterval with current selectedTime when stagedQueryId changes', () => { + const { rerender } = renderHook( + ({ id }: { id: string | undefined }) => useSyncTimeOnStagedQueryChange(id), + { initialProps: { id: 'first-id' as string | undefined } }, + ); + + expect(mockDispatch).not.toHaveBeenCalled(); + + rerender({ id: 'second-id' }); + + expect(mockedUpdateTimeInterval).toHaveBeenCalledTimes(1); + expect(mockedUpdateTimeInterval).toHaveBeenCalledWith('1h'); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'UPDATE_TIME_INTERVAL_THUNK', + payload: '1h', + }); + }); + + it('does not dispatch when selectedTime is "custom" even if stagedQueryId changes', () => { + setSelectedTime('custom'); + + const { rerender } = renderHook( + ({ id }: { id: string | undefined }) => useSyncTimeOnStagedQueryChange(id), + { initialProps: { id: 'first-id' as string | undefined } }, + ); + + rerender({ id: 'second-id' }); + + expect(mockDispatch).not.toHaveBeenCalled(); + expect(mockedUpdateTimeInterval).not.toHaveBeenCalled(); + }); + + it('does not dispatch when only selectedTime changes (stagedQueryId stable)', () => { + const { rerender } = renderHook( + ({ id }: { id: string | undefined }) => useSyncTimeOnStagedQueryChange(id), + { initialProps: { id: 'stable-id' as string | undefined } }, + ); + + setSelectedTime('5m'); + rerender({ id: 'stable-id' }); + + expect(mockDispatch).not.toHaveBeenCalled(); + }); + + it('dispatches once per distinct stagedQueryId change', () => { + const { rerender } = renderHook( + ({ id }: { id: string | undefined }) => useSyncTimeOnStagedQueryChange(id), + { initialProps: { id: 'a' as string | undefined } }, + ); + + rerender({ id: 'b' }); + rerender({ id: 'c' }); + rerender({ id: 'c' }); // no change — should not dispatch again + + expect(mockDispatch).toHaveBeenCalledTimes(2); + }); + + it('does not dispatch when stagedQueryId transitions from defined to undefined', () => { + const { rerender } = renderHook( + ({ id }: { id: string | undefined }) => useSyncTimeOnStagedQueryChange(id), + { initialProps: { id: 'first-id' as string | undefined } }, + ); + + rerender({ id: undefined }); + + expect(mockDispatch).not.toHaveBeenCalled(); + }); + + it('uses the latest selectedTime at the moment of stagedQueryId change', () => { + const { rerender } = renderHook( + ({ id }: { id: string | undefined }) => useSyncTimeOnStagedQueryChange(id), + { initialProps: { id: 'a' as string | undefined } }, + ); + + setSelectedTime('15m'); + rerender({ id: 'b' }); + + expect(mockedUpdateTimeInterval).toHaveBeenCalledWith('15m'); + }); +}); diff --git a/frontend/src/hooks/queryBuilder/useSyncTimeOnStagedQueryChange.ts b/frontend/src/hooks/queryBuilder/useSyncTimeOnStagedQueryChange.ts new file mode 100644 index 00000000000..b130e3b7f2d --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useSyncTimeOnStagedQueryChange.ts @@ -0,0 +1,36 @@ +import { useEffect, useRef } from 'react'; +// eslint-disable-next-line no-restricted-imports +import { useDispatch, useSelector } from 'react-redux'; +import { UpdateTimeInterval } from 'store/actions'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +// Push fresh min/max back into Redux whenever the staged query changes for a +// relative time interval. The data hooks that read minTime/maxTime from Redux +// otherwise keep refetching with the originally frozen window and the time +// picker displays a stale absolute range. +// ref - SigNoz/signoz#8277 +export function useSyncTimeOnStagedQueryChange( + stagedQueryId: string | undefined, +): void { + const dispatch = useDispatch(); + const selectedTime = useSelector( + (state) => state.globalTime.selectedTime, + ); + const prevStagedQueryIdRef = useRef(); + + useEffect(() => { + const prevId = prevStagedQueryIdRef.current; + const currentId = stagedQueryId; + prevStagedQueryIdRef.current = currentId; + + if ( + prevId !== undefined && + currentId !== undefined && + prevId !== currentId && + selectedTime !== 'custom' + ) { + dispatch(UpdateTimeInterval(selectedTime)); + } + }, [stagedQueryId, selectedTime, dispatch]); +} diff --git a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/AddSpanToFunnelModal/AddSpanToFunnelModal.module.scss b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/AddSpanToFunnelModal/AddSpanToFunnelModal.module.scss index e4fc6394f4c..94a04d54d12 100644 --- a/frontend/src/pages/TraceDetailsV3/TraceWaterfall/AddSpanToFunnelModal/AddSpanToFunnelModal.module.scss +++ b/frontend/src/pages/TraceDetailsV3/TraceWaterfall/AddSpanToFunnelModal/AddSpanToFunnelModal.module.scss @@ -119,6 +119,7 @@ gap: 12px; margin-bottom: 14px; align-items: center; + background: var(--l3-background); } .searchInput { @@ -126,16 +127,13 @@ padding: 6px 8px; background: var(--l3-background); - :global(.ant-input-prefix) { - height: 18px; - margin-inline-end: 6px; + height: 18px; + margin-inline-end: 6px; - svg { - opacity: 0.4; - } + svg { + opacity: 0.4; } - &, input { font-size: 14px; line-height: 18px;