Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 4 additions & 19 deletions frontend/src/container/LogsExplorerViews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -90,10 +88,9 @@ function LogsExplorerViewsContainer({
DEFAULT_PER_PAGE_VALUE,
);

const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);

const currentMinTimeRef = useRef<number>(minTime);

Expand Down Expand Up @@ -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: '' },
Expand All @@ -360,8 +347,6 @@ function LogsExplorerViewsContainer({
minTime,
activeLogId,
selectedPanelType,
dispatch,
selectedTime,
maxTime,
orderBy,
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,21 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));

jest.mock(
'container/TopNav/DateTimeSelectionV2/index.tsx',
() =>
function MockDateTimeSelection(): JSX.Element {
return <div>MockDateTimeSelection</div>;
},
);
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 <div>MockDateTimeSelection</div>;
};
});

jest.mock(
'container/LogsExplorerChart',
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -244,7 +246,7 @@
padding: 2px 6px;
}

.close-icon {
> button {
display: flex;
align-items: center;
justify-content: center;
Expand All @@ -259,40 +261,40 @@
&.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);
}
}

&.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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,7 @@ function QueryBuilderSearchV2(
>
<Tooltip title={chipValue}>
<TypographyText
className="qb-tag-text"
$isInNin={isInNin}
$isEnabled={!!searchValue}
onClick={(): void => {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -187,6 +188,8 @@ function DateTimeSelection({

const { stagedQuery, currentQuery, initQueryBuilderData } = useQueryBuilder();

useSyncTimeOnStagedQueryChange(stagedQuery?.id);

const getInputLabel = (
startTime?: Dayjs,
endTime?: Dayjs,
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
36 changes: 36 additions & 0 deletions frontend/src/hooks/queryBuilder/useSyncTimeOnStagedQueryChange.ts
Original file line number Diff line number Diff line change
@@ -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<AppState, GlobalReducer['selectedTime']>(
(state) => state.globalTime.selectedTime,
);
const prevStagedQueryIdRef = useRef<string | undefined>();

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]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,21 @@
gap: 12px;
margin-bottom: 14px;
align-items: center;
background: var(--l3-background);
}

.searchInput {
flex: 1;
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;
Expand Down
Loading