diff --git a/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss b/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss
index 38d8456467..adf6e98269 100644
--- a/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss
+++ b/frontend/src/components/CustomTimePicker/CustomTimePicker.styles.scss
@@ -1,31 +1,6 @@
.custom-time-picker {
display: flex;
- flex-direction: row;
- align-items: center;
- gap: 4px;
-
- .zoom-out-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- color: var(--foreground);
- border: 1px solid var(--border);
- border-radius: 2px;
- box-shadow: none;
- padding: 10px;
- height: 33px;
-
- &:hover:not(:disabled) {
- color: var(--bg-vanilla-100);
- background: var(--primary);
- }
-
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- }
+ flex-direction: column;
.timeSelection-input {
&:hover {
diff --git a/frontend/src/components/CustomTimePicker/CustomTimePicker.test.tsx b/frontend/src/components/CustomTimePicker/CustomTimePicker.test.tsx
index 78387187ac..a752f49361 100644
--- a/frontend/src/components/CustomTimePicker/CustomTimePicker.test.tsx
+++ b/frontend/src/components/CustomTimePicker/CustomTimePicker.test.tsx
@@ -16,15 +16,6 @@ jest.mock('react-router-dom', () => {
};
});
-jest.mock('react-redux', () => ({
- ...jest.requireActual('react-redux'),
- useDispatch: jest.fn(() => jest.fn()),
- useSelector: jest.fn(() => ({
- minTime: 0,
- maxTime: Date.now(),
- })),
-}));
-
jest.mock('providers/Timezone', () => {
const actual = jest.requireActual('providers/Timezone');
diff --git a/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx b/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx
index 008f63357f..a1b2c582ec 100644
--- a/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx
+++ b/frontend/src/components/CustomTimePicker/CustomTimePicker.tsx
@@ -7,11 +7,9 @@ import {
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
-import { Button } from '@signozhq/button';
import { Input, InputRef, Popover, Tooltip } from 'antd';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
-import { QueryParams } from 'constants/query';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
FixedDurationSuggestionOptions,
@@ -19,11 +17,9 @@ import {
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/constants';
import dayjs from 'dayjs';
-import { useZoomOut } from 'hooks/useZoomOut';
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
-import { isZoomOutDisabled } from 'lib/zoomOutUtils';
import { defaultTo, isFunction, noop } from 'lodash-es';
-import { ChevronDown, ChevronUp, ZoomOut } from 'lucide-react';
+import { ChevronDown, ChevronUp } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { getTimeDifference, validateEpochRange } from 'utils/epochUtils';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -70,8 +66,6 @@ interface CustomTimePickerProps {
showRecentlyUsed?: boolean;
minTime: number;
maxTime: number;
- /** When true, zoom-out button is hidden (e.g. in drawer/modal time selection) */
- isModalTimeSelection?: boolean;
}
function CustomTimePicker({
@@ -94,7 +88,6 @@ function CustomTimePicker({
showRecentlyUsed = true,
minTime,
maxTime,
- isModalTimeSelection = false,
}: CustomTimePickerProps): JSX.Element {
const [
selectedTimePlaceholderValue,
@@ -123,14 +116,6 @@ function CustomTimePicker({
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
- const durationMs = (maxTime - minTime) / 1e6;
- const zoomOutDisabled = showLiveLogs || isZoomOutDisabled(durationMs);
-
- const handleZoomOut = useZoomOut({
- isDisabled: zoomOutDisabled,
- urlParamsToDelete: [QueryParams.activeLogId],
- });
-
// function to get selected time in Last 1m, Last 2h, Last 3d, Last 4w format
// 1m, 2h, 3d, 4w -> Last 1 minute, Last 2 hours, Last 3 days, Last 4 weeks
const getSelectedTimeRangeLabelInRelativeFormat = (
@@ -646,23 +631,6 @@ function CustomTimePicker({
/>
- {!showLiveLogs && !isModalTimeSelection && (
-
-
- }
- />
-
-
- )}
);
}
diff --git a/frontend/src/components/CustomTimePicker/__tests__/customTimePickerZoomOut.test.tsx b/frontend/src/components/CustomTimePicker/__tests__/customTimePickerZoomOut.test.tsx
deleted file mode 100644
index a73c0e47d6..0000000000
--- a/frontend/src/components/CustomTimePicker/__tests__/customTimePickerZoomOut.test.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { QueryParams } from 'constants/query';
-import { GlobalReducer } from 'types/reducer/globalTime';
-
-import CustomTimePicker from '../CustomTimePicker';
-
-const MS_PER_MIN = 60 * 1000;
-const NOW_MS = 1705312800000;
-
-const mockDispatch = jest.fn();
-const mockSafeNavigate = jest.fn();
-const mockUrlQueryDelete = jest.fn();
-const mockUrlQuerySet = jest.fn();
-
-interface MockAppState {
- globalTime: Pick;
-}
-
-jest.mock('react-redux', () => ({
- useDispatch: (): jest.Mock => mockDispatch,
- useSelector: (selector: (state: MockAppState) => unknown): unknown => {
- const mockState: MockAppState = {
- globalTime: {
- minTime: (NOW_MS - 15 * MS_PER_MIN) * 1e6,
- maxTime: NOW_MS * 1e6,
- },
- };
- return selector(mockState);
- },
-}));
-
-jest.mock('hooks/useSafeNavigate', () => ({
- useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
- safeNavigate: mockSafeNavigate,
- }),
-}));
-
-interface MockUrlQuery {
- delete: typeof mockUrlQueryDelete;
- set: typeof mockUrlQuerySet;
- get: () => null;
- toString: () => string;
-}
-
-jest.mock('hooks/useUrlQuery', () => ({
- __esModule: true,
- default: (): MockUrlQuery => ({
- delete: mockUrlQueryDelete,
- set: mockUrlQuerySet,
- get: (): null => null,
- toString: (): string => 'relativeTime=45m',
- }),
-}));
-
-jest.mock('providers/Timezone', () => ({
- useTimezone: (): { timezone: { value: string; offset: string } } => ({
- timezone: { value: 'UTC', offset: 'UTC' },
- }),
-}));
-
-jest.mock('react-router-dom', () => ({
- useLocation: (): { pathname: string } => ({ pathname: '/logs-explorer' }),
-}));
-
-const MS_PER_DAY = 24 * 60 * 60 * 1000;
-const now = Date.now();
-const defaultProps = {
- onSelect: jest.fn(),
- onError: jest.fn(),
- selectedValue: '15m',
- selectedTime: '15m',
- onValidCustomDateChange: jest.fn(),
- open: false,
- setOpen: jest.fn(),
- items: [
- { value: '15m', label: 'Last 15 minutes' },
- { value: '1h', label: 'Last 1 hour' },
- ],
- minTime: (now - 15 * 60 * 1000) * 1e6,
- maxTime: now * 1e6,
-};
-
-describe('CustomTimePicker - zoom out button', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- jest.spyOn(Date, 'now').mockReturnValue(NOW_MS);
- });
-
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
- it('should render zoom out button when showLiveLogs is false', () => {
- render();
-
- expect(screen.getByTestId('zoom-out-btn')).toBeInTheDocument();
- });
-
- it('should not render zoom out button when showLiveLogs is true', () => {
- render();
-
- expect(screen.queryByTestId('zoom-out-btn')).not.toBeInTheDocument();
- });
-
- it('should not render zoom out button when isModalTimeSelection is true', () => {
- render(
- ,
- );
-
- expect(screen.queryByTestId('zoom-out-btn')).not.toBeInTheDocument();
- });
-
- it('should call handleZoomOut when zoom out button is clicked', async () => {
- render();
-
- const zoomOutBtn = screen.getByTestId('zoom-out-btn');
- await userEvent.click(zoomOutBtn);
-
- expect(mockDispatch).toHaveBeenCalled();
- expect(mockUrlQuerySet).toHaveBeenCalledWith(QueryParams.relativeTime, '45m');
- expect(mockSafeNavigate).toHaveBeenCalledWith(
- expect.stringMatching(/\/logs-explorer\?relativeTime=45m/),
- );
- });
-
- it('should use real ladder logic: 15m range zooms to 45m preset and updates URL', async () => {
- render();
-
- const zoomOutBtn = screen.getByTestId('zoom-out-btn');
- await userEvent.click(zoomOutBtn);
-
- expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.startTime);
- expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.endTime);
- expect(mockUrlQuerySet).toHaveBeenCalledWith(QueryParams.relativeTime, '45m');
- expect(mockSafeNavigate).toHaveBeenCalledWith(
- expect.stringMatching(/\/logs-explorer\?relativeTime=45m/),
- );
- expect(mockDispatch).toHaveBeenCalled();
- });
-
- it('should delete activeLogId when zoom out is clicked', async () => {
- render();
-
- const zoomOutBtn = screen.getByTestId('zoom-out-btn');
- await userEvent.click(zoomOutBtn);
-
- expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.activeLogId);
- });
-
- it('should disable zoom button when time range is >= 1 month', () => {
- const now = Date.now();
- render(
- ,
- );
-
- const zoomOutBtn = screen.getByTestId('zoom-out-btn');
- expect(zoomOutBtn).toBeDisabled();
- });
-});
diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx
index 6daa46c658..2072a3be0a 100644
--- a/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx
+++ b/frontend/src/container/TopNav/DateTimeSelectionV2/index.tsx
@@ -30,7 +30,6 @@ import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import { addCustomTimeRange } from 'utils/customTimeRangeUtils';
-import { persistTimeDurationForRoute } from 'utils/metricsTimeStorageUtils';
import { normalizeTimeToMs } from 'utils/timeUtils';
import { v4 as uuid } from 'uuid';
@@ -235,7 +234,20 @@ function DateTimeSelection({
const updateLocalStorageForRoutes = useCallback(
(value: Time | string): void => {
- persistTimeDurationForRoute(location.pathname, String(value));
+ const preRoutes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
+ if (preRoutes !== null) {
+ const preRoutesObject = JSON.parse(preRoutes);
+
+ const preRoute = {
+ ...preRoutesObject,
+ };
+ preRoute[location.pathname] = value;
+
+ setLocalStorageKey(
+ LOCALSTORAGE.METRICS_TIME_IN_DURATION,
+ JSON.stringify(preRoute),
+ );
+ }
},
[location.pathname],
);
@@ -726,7 +738,6 @@ function DateTimeSelection({
showRecentlyUsed={showRecentlyUsed}
minTime={minTimeForDateTimePicker}
maxTime={maxTimeForDateTimePicker}
- isModalTimeSelection={isModalTimeSelection}
/>
{showAutoRefresh && selectedTime !== 'custom' && (
diff --git a/frontend/src/hooks/__tests__/useZoomOut.test.ts b/frontend/src/hooks/__tests__/useZoomOut.test.ts
deleted file mode 100644
index 67b61aaa9d..0000000000
--- a/frontend/src/hooks/__tests__/useZoomOut.test.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-import { act, renderHook } from '@testing-library/react';
-import { QueryParams } from 'constants/query';
-import { GlobalReducer } from 'types/reducer/globalTime';
-
-import { useZoomOut } from '../useZoomOut';
-
-const mockDispatch = jest.fn();
-const mockSafeNavigate = jest.fn();
-const mockUrlQueryDelete = jest.fn();
-const mockUrlQuerySet = jest.fn();
-const mockUrlQueryToString = jest.fn(() => '');
-
-interface MockAppState {
- globalTime: Pick;
-}
-
-jest.mock('react-redux', () => ({
- useDispatch: (): jest.Mock => mockDispatch,
- useSelector: (selector: (state: MockAppState) => T): T => {
- const mockState: MockAppState = {
- globalTime: {
- minTime: 15 * 60 * 1000 * 1e6, // 15 min in nanoseconds
- maxTime: 30 * 60 * 1000 * 1e6, // 30 min in nanoseconds (mock for getNextZoomOutRange)
- },
- };
- return selector(mockState);
- },
-}));
-
-jest.mock('react-router-dom', () => ({
- useLocation: (): { pathname: string } => ({ pathname: '/logs-explorer' }),
-}));
-
-jest.mock('hooks/useSafeNavigate', () => ({
- useSafeNavigate: (): { safeNavigate: typeof mockSafeNavigate } => ({
- safeNavigate: mockSafeNavigate,
- }),
-}));
-
-interface MockUrlQuery {
- delete: typeof mockUrlQueryDelete;
- set: typeof mockUrlQuerySet;
- get: () => null;
- toString: typeof mockUrlQueryToString;
-}
-
-jest.mock('hooks/useUrlQuery', () => ({
- __esModule: true,
- default: (): MockUrlQuery => ({
- delete: mockUrlQueryDelete,
- set: mockUrlQuerySet,
- get: (): null => null,
- toString: mockUrlQueryToString,
- }),
-}));
-
-const mockGetNextZoomOutRange = jest.fn();
-jest.mock('lib/zoomOutUtils', () => ({
- getNextZoomOutRange: (
- ...args: unknown[]
- ): ReturnType =>
- mockGetNextZoomOutRange(...args),
-}));
-
-describe('useZoomOut', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- mockUrlQueryToString.mockReturnValue('relativeTime=45m');
- });
-
- it('should do nothing when isDisabled is true', () => {
- const { result } = renderHook(() => useZoomOut({ isDisabled: true }));
-
- act(() => {
- result.current();
- });
-
- expect(mockGetNextZoomOutRange).not.toHaveBeenCalled();
- expect(mockDispatch).not.toHaveBeenCalled();
- expect(mockSafeNavigate).not.toHaveBeenCalled();
- });
-
- it('should do nothing when getNextZoomOutRange returns null', () => {
- mockGetNextZoomOutRange.mockReturnValue(null);
-
- const { result } = renderHook(() => useZoomOut());
-
- act(() => {
- result.current();
- });
-
- expect(mockGetNextZoomOutRange).toHaveBeenCalled();
- expect(mockDispatch).not.toHaveBeenCalled();
- expect(mockSafeNavigate).not.toHaveBeenCalled();
- });
-
- it('should dispatch preset and update URL when result has preset', () => {
- mockGetNextZoomOutRange.mockReturnValue({
- range: [1000, 2000],
- preset: '45m',
- });
-
- const { result } = renderHook(() => useZoomOut());
-
- act(() => {
- result.current();
- });
-
- expect(mockDispatch).toHaveBeenCalledWith(expect.any(Function));
- expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.startTime);
- expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.endTime);
- expect(mockUrlQuerySet).toHaveBeenCalledWith(QueryParams.relativeTime, '45m');
- expect(mockSafeNavigate).toHaveBeenCalledWith(
- expect.stringContaining('/logs-explorer'),
- );
- });
-
- it('should dispatch custom range and update URL when result has no preset', () => {
- mockGetNextZoomOutRange.mockReturnValue({
- range: [1000000, 2000000],
- preset: null,
- });
-
- const { result } = renderHook(() => useZoomOut());
-
- act(() => {
- result.current();
- });
-
- expect(mockDispatch).toHaveBeenCalledWith(expect.any(Function));
- expect(mockUrlQuerySet).toHaveBeenCalledWith(
- QueryParams.startTime,
- '1000000',
- );
- expect(mockUrlQuerySet).toHaveBeenCalledWith(QueryParams.endTime, '2000000');
- expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.relativeTime);
- expect(mockSafeNavigate).toHaveBeenCalledWith(
- expect.stringContaining('/logs-explorer'),
- );
- });
-
- it('should delete urlParamsToDelete when provided', () => {
- mockGetNextZoomOutRange.mockReturnValue({
- range: [1000, 2000],
- preset: '45m',
- });
-
- const { result } = renderHook(() =>
- useZoomOut({
- urlParamsToDelete: [QueryParams.activeLogId],
- }),
- );
-
- act(() => {
- result.current();
- });
-
- expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.activeLogId);
- });
-});
diff --git a/frontend/src/hooks/useZoomOut.ts b/frontend/src/hooks/useZoomOut.ts
deleted file mode 100644
index 03432b88b6..0000000000
--- a/frontend/src/hooks/useZoomOut.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { useCallback, useRef } from 'react';
-// eslint-disable-next-line no-restricted-imports
-import { useDispatch, useSelector } from 'react-redux';
-import { useLocation } from 'react-router-dom';
-import { QueryParams } from 'constants/query';
-import { useSafeNavigate } from 'hooks/useSafeNavigate';
-import useUrlQuery from 'hooks/useUrlQuery';
-import { getNextZoomOutRange } from 'lib/zoomOutUtils';
-import { UpdateTimeInterval } from 'store/actions';
-import { AppState } from 'store/reducers';
-import { GlobalReducer } from 'types/reducer/globalTime';
-import { persistTimeDurationForRoute } from 'utils/metricsTimeStorageUtils';
-
-export interface UseZoomOutOptions {
- /** When true, the zoom out handler does nothing (e.g. when live logs are enabled) */
- isDisabled?: boolean;
- /** URL params to delete when zooming out (e.g. [QueryParams.activeLogId] for logs) */
- urlParamsToDelete?: string[];
-}
-
-/**
- * Reusable hook for zoom-out functionality in explorers (logs, traces, etc.).
- * Computes the next time range using the zoom-out ladder, updates Redux global time,
- * and navigates with the new URL params.
- */
-const EMPTY_PARAMS: string[] = [];
-
-export function useZoomOut(options: UseZoomOutOptions = {}): () => void {
- const { isDisabled = false, urlParamsToDelete = EMPTY_PARAMS } = options;
- const urlParamsToDeleteRef = useRef(urlParamsToDelete);
- urlParamsToDeleteRef.current = urlParamsToDelete;
-
- const dispatch = useDispatch();
- const { minTime, maxTime } = useSelector(
- (state) => state.globalTime,
- );
- const urlQuery = useUrlQuery();
- const location = useLocation();
- const { safeNavigate } = useSafeNavigate();
-
- return useCallback((): void => {
- if (isDisabled) {
- return;
- }
- const minMs = Math.floor((minTime ?? 0) / 1e6);
- const maxMs = Math.floor((maxTime ?? 0) / 1e6);
- const result = getNextZoomOutRange(minMs, maxMs);
- if (!result) {
- return;
- }
- const [newStartMs, newEndMs] = result.range;
- const { preset } = result;
-
- if (preset) {
- dispatch(UpdateTimeInterval(preset));
- urlQuery.delete(QueryParams.startTime);
- urlQuery.delete(QueryParams.endTime);
- urlQuery.set(QueryParams.relativeTime, preset);
- persistTimeDurationForRoute(location.pathname, preset);
- } else {
- dispatch(UpdateTimeInterval('custom', [newStartMs, newEndMs]));
- urlQuery.set(QueryParams.startTime, String(newStartMs));
- urlQuery.set(QueryParams.endTime, String(newEndMs));
- urlQuery.delete(QueryParams.relativeTime);
- }
- for (const param of urlParamsToDeleteRef.current) {
- urlQuery.delete(param);
- }
- safeNavigate(`${location.pathname}?${urlQuery.toString()}`);
- }, [
- dispatch,
- isDisabled,
- location.pathname,
- maxTime,
- minTime,
- safeNavigate,
- urlQuery,
- ]);
-}
diff --git a/frontend/src/lib/__tests__/zoomOutUtils.test.ts b/frontend/src/lib/__tests__/zoomOutUtils.test.ts
deleted file mode 100644
index d459174d05..0000000000
--- a/frontend/src/lib/__tests__/zoomOutUtils.test.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-import {
- getNextDurationInLadder,
- getNextZoomOutRange,
- isZoomOutDisabled,
- ZoomOutResult,
-} from '../zoomOutUtils';
-
-const MS_PER_MIN = 60 * 1000;
-const MS_PER_HOUR = 60 * MS_PER_MIN;
-const MS_PER_DAY = 24 * MS_PER_HOUR;
-const MS_PER_WEEK = 7 * MS_PER_DAY;
-
-// Fixed "now" for deterministic tests: 2024-01-15 12:00:00 UTC
-const NOW_MS = 1705312800000;
-
-describe('zoomOutUtils', () => {
- beforeEach(() => {
- jest.spyOn(Date, 'now').mockReturnValue(NOW_MS);
- });
-
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
- describe('getNextDurationInLadder', () => {
- it('should use 3x zoom out below 15m until reaching 15m', () => {
- expect(getNextDurationInLadder(1 * MS_PER_MIN)).toBe(3 * MS_PER_MIN);
- expect(getNextDurationInLadder(2 * MS_PER_MIN)).toBe(6 * MS_PER_MIN);
- expect(getNextDurationInLadder(3 * MS_PER_MIN)).toBe(9 * MS_PER_MIN);
- expect(getNextDurationInLadder(4 * MS_PER_MIN)).toBe(12 * MS_PER_MIN);
- expect(getNextDurationInLadder(5 * MS_PER_MIN)).toBe(15 * MS_PER_MIN); // cap at 15m
- expect(getNextDurationInLadder(6 * MS_PER_MIN)).toBe(15 * MS_PER_MIN); // 18m capped
- });
-
- it('should return next step for each ladder rung from 15m onward', () => {
- expect(getNextDurationInLadder(10 * MS_PER_MIN)).toBe(15 * MS_PER_MIN);
- expect(getNextDurationInLadder(15 * MS_PER_MIN)).toBe(45 * MS_PER_MIN);
- expect(getNextDurationInLadder(45 * MS_PER_MIN)).toBe(2 * MS_PER_HOUR);
- expect(getNextDurationInLadder(2 * MS_PER_HOUR)).toBe(7 * MS_PER_HOUR);
- expect(getNextDurationInLadder(7 * MS_PER_HOUR)).toBe(21 * MS_PER_HOUR);
- expect(getNextDurationInLadder(21 * MS_PER_HOUR)).toBe(1 * MS_PER_DAY);
- expect(getNextDurationInLadder(1 * MS_PER_DAY)).toBe(2 * MS_PER_DAY);
- expect(getNextDurationInLadder(2 * MS_PER_DAY)).toBe(3 * MS_PER_DAY);
- expect(getNextDurationInLadder(3 * MS_PER_DAY)).toBe(1 * MS_PER_WEEK);
- expect(getNextDurationInLadder(1 * MS_PER_WEEK)).toBe(2 * MS_PER_WEEK);
- expect(getNextDurationInLadder(2 * MS_PER_WEEK)).toBe(30 * MS_PER_DAY);
- });
-
- it('should return MAX when at or past 1 month (no wrap)', () => {
- expect(getNextDurationInLadder(30 * MS_PER_DAY)).toBe(30 * MS_PER_DAY);
- expect(getNextDurationInLadder(31 * MS_PER_DAY)).toBe(30 * MS_PER_DAY);
- });
-
- it('should return next step for duration between ladder rungs', () => {
- expect(getNextDurationInLadder(1 * MS_PER_HOUR)).toBe(2 * MS_PER_HOUR);
- expect(getNextDurationInLadder(5 * MS_PER_HOUR)).toBe(7 * MS_PER_HOUR);
- expect(getNextDurationInLadder(12 * MS_PER_HOUR)).toBe(21 * MS_PER_HOUR);
- });
- });
-
- describe('getNextZoomOutRange', () => {
- it('should return null when duration is zero or negative', () => {
- expect(getNextZoomOutRange(NOW_MS, NOW_MS)).toBeNull();
- expect(getNextZoomOutRange(NOW_MS, NOW_MS - 1000)).toBeNull();
- });
-
- it('should return center-anchored range and preset=null when new end does not exceed now (Phase 1)', () => {
- // 15m range centered well before now so zoom to 45m keeps end <= now
- // Center at now-30m: end = center + 22.5m = now - 7.5m <= now
- const centerMs = NOW_MS - 30 * MS_PER_MIN;
- const start15m = centerMs - 7.5 * MS_PER_MIN;
- const end15m = centerMs + 7.5 * MS_PER_MIN;
- const result = getNextZoomOutRange(start15m, end15m) as ZoomOutResult;
-
- expect(result).not.toBeNull();
- expect(result.preset).toBeNull(); // Phase 1: preserve center-anchored range, avoid GetMinMax "last X from now"
- const [newStart, newEnd] = result.range;
- expect(newEnd - newStart).toBe(45 * MS_PER_MIN);
- const newCenter = (newStart + newEnd) / 2;
- expect(Math.abs(newCenter - centerMs)).toBeLessThan(2000);
- expect(newEnd).toBeLessThanOrEqual(NOW_MS + 1000);
- });
-
- it('should return end-anchored range when new end would exceed now (Phase 2)', () => {
- // 22hr range ending at now - zoom to 1d (24hr) would push end past now
- // Next ladder step from 22hr is 1d
- const start22h = NOW_MS - 22 * MS_PER_HOUR;
- const end22h = NOW_MS;
- const result = getNextZoomOutRange(start22h, end22h) as ZoomOutResult;
-
- expect(result).not.toBeNull();
- expect(result.preset).toBe('1d');
- const [newStart, newEnd] = result.range;
- expect(newEnd).toBe(NOW_MS); // End anchored at now
- expect(newStart).toBe(NOW_MS - 1 * MS_PER_DAY);
- });
-
- it('should return correct preset for each ladder step', () => {
- const presets: [number, number, string][] = [
- [15 * MS_PER_MIN, 0, '45m'],
- [45 * MS_PER_MIN, 0, '2h'],
- [2 * MS_PER_HOUR, 0, '7h'],
- [7 * MS_PER_HOUR, 0, '21h'],
- [21 * MS_PER_HOUR, 0, '1d'],
- [1 * MS_PER_DAY, 0, '2d'],
- [2 * MS_PER_DAY, 0, '3d'],
- [3 * MS_PER_DAY, 0, '1w'],
- [1 * MS_PER_WEEK, 0, '2w'],
- [2 * MS_PER_WEEK, 0, '1month'],
- ];
-
- presets.forEach(([durationMs, offset, expectedPreset]) => {
- const end = NOW_MS - offset;
- const start = end - durationMs;
- const result = getNextZoomOutRange(start, end);
- expect(result?.preset).toBe(expectedPreset);
- });
- });
-
- it('isZoomOutDisabled returns true when duration >= 1 month', () => {
- expect(isZoomOutDisabled(30 * MS_PER_DAY)).toBe(true);
- expect(isZoomOutDisabled(31 * MS_PER_DAY)).toBe(true);
- expect(isZoomOutDisabled(29 * MS_PER_DAY)).toBe(false);
- expect(isZoomOutDisabled(15 * MS_PER_MIN)).toBe(false);
- });
-
- it('should return null when at 1 month (no zoom out beyond max)', () => {
- const start1m = NOW_MS - 30 * MS_PER_DAY;
- const end1m = NOW_MS;
- const result = getNextZoomOutRange(start1m, end1m);
-
- expect(result).toBeNull();
- });
-
- it('should zoom out 3x from 5m range to 15m then continue with ladder', () => {
- // 5m range ending at now → 3x = 15m
- const start5m = NOW_MS - 5 * MS_PER_MIN;
- const end5m = NOW_MS;
- const result = getNextZoomOutRange(start5m, end5m) as ZoomOutResult;
-
- expect(result).not.toBeNull();
- expect(result.preset).toBe('15m');
- const [newStart, newEnd] = result.range;
- expect(newEnd - newStart).toBe(15 * MS_PER_MIN);
- });
- });
-});
diff --git a/frontend/src/lib/zoomOutUtils.ts b/frontend/src/lib/zoomOutUtils.ts
deleted file mode 100644
index 77da376c79..0000000000
--- a/frontend/src/lib/zoomOutUtils.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-/**
- * Custom Time Picker zoom-out ladder:
- * - Until 1 day: 15m → 45m → 2hr → 7hr → 21hr
- * - Then fixed: 1d → 2d → 3d → 1w → 2w → 1m
- * - At 1 month: zoom out is disabled (max range)
- */
-
-import type {
- CustomTimeType,
- Time,
-} from 'container/TopNav/DateTimeSelectionV2/types';
-
-const MS_PER_MIN = 60 * 1000;
-const MS_PER_HOUR = 60 * MS_PER_MIN;
-const MS_PER_DAY = 24 * MS_PER_HOUR;
-const MS_PER_WEEK = 7 * MS_PER_DAY;
-
-const ZOOM_OUT_LADDER_MS: number[] = [
- 15 * MS_PER_MIN, // 15m
- 45 * MS_PER_MIN, // 45m
- 2 * MS_PER_HOUR, // 2hr
- 7 * MS_PER_HOUR, // 7hr
- 21 * MS_PER_HOUR, // 21hr
- 1 * MS_PER_DAY, // 1d
- 2 * MS_PER_DAY, // 2d
- 3 * MS_PER_DAY, // 3d
- 1 * MS_PER_WEEK, // 1w
- 2 * MS_PER_WEEK, // 2w
- 30 * MS_PER_DAY, // 1m
-];
-
-const LADDER_LAST_INDEX = ZOOM_OUT_LADDER_MS.length - 1;
-const MAX_DURATION = ZOOM_OUT_LADDER_MS[LADDER_LAST_INDEX];
-const MIN_LADDER_DURATION_MS = ZOOM_OUT_LADDER_MS[0]; // 15m - below this we use 3x
-
-export const MAX_ZOOM_OUT_DURATION_MS = MAX_DURATION;
-
-/** Returns true when zoom out should be disabled (range at or beyond 1 month) */
-export function isZoomOutDisabled(durationMs: number): boolean {
- return durationMs >= MAX_ZOOM_OUT_DURATION_MS;
-}
-
-/** Preset labels for ladder steps supported by GetMinMax (shows "Last 15 minutes" etc. instead of "Custom") */
-const PRESET_FOR_DURATION_MS: Record = {
- [15 * MS_PER_MIN]: '15m',
- [45 * MS_PER_MIN]: '45m',
- [2 * MS_PER_HOUR]: '2h',
- [7 * MS_PER_HOUR]: '7h',
- [21 * MS_PER_HOUR]: '21h',
- [1 * MS_PER_DAY]: '1d',
- [2 * MS_PER_DAY]: '2d',
- [3 * MS_PER_DAY]: '3d',
- [1 * MS_PER_WEEK]: '1w',
- [2 * MS_PER_WEEK]: '2w',
- [30 * MS_PER_DAY]: '1month',
-};
-
-/**
- * Returns the next duration in the zoom-out ladder for the given current duration.
- * Below 15m: zoom out 3x until we reach 15m, then continue with the ladder.
- * If at or past 1 month, returns MAX_DURATION (no zoom out - button is disabled).
- */
-export function getNextDurationInLadder(durationMs: number): number {
- if (durationMs >= MAX_DURATION) {
- return MAX_DURATION; // No zoom out beyond 1 month
- }
-
- // Below 15m: zoom out 3x until we reach 15m
- if (durationMs < MIN_LADDER_DURATION_MS) {
- const next = durationMs * 3;
- return Math.min(next, MIN_LADDER_DURATION_MS);
- }
-
- // At or above 15m: use the fixed ladder
- for (let i = 0; i < ZOOM_OUT_LADDER_MS.length; i++) {
- if (ZOOM_OUT_LADDER_MS[i] > durationMs) {
- return ZOOM_OUT_LADDER_MS[i];
- }
- }
-
- return MAX_DURATION;
-}
-
-export interface ZoomOutResult {
- range: [number, number];
- /** Preset key (e.g. '15m') when range matches a preset - use for display instead of "Custom Date Range" */
- preset: Time | CustomTimeType | null;
-}
-
-/**
- * Computes the next zoomed-out time range.
- * Phase 1 (center-anchored): While new end <= now, expand from center.
- * Phase 2 (end-anchored at now): When new end would exceed now, anchor end at now and move start backward.
- *
- * @returns ZoomOutResult with range and preset (or null if no change)
- */
-export function getNextZoomOutRange(
- startMs: number,
- endMs: number,
-): ZoomOutResult | null {
- const nowMs = Date.now();
- const durationMs = endMs - startMs;
-
- if (durationMs <= 0) {
- return null;
- }
-
- const newDurationMs = getNextDurationInLadder(durationMs);
-
- // No zoom out when already at max (1 month)
- if (newDurationMs <= durationMs) {
- return null;
- }
- const centerMs = startMs + durationMs / 2;
- const computedEndMs = centerMs + newDurationMs / 2;
-
- let newStartMs: number;
- let newEndMs: number;
-
- const isPhase1 = computedEndMs <= nowMs;
- if (isPhase1) {
- // Phase 1: center-anchored (historical range not ending at now)
- newStartMs = centerMs - newDurationMs / 2;
- newEndMs = computedEndMs;
- } else {
- // Phase 2: end-anchored at now
- newStartMs = nowMs - newDurationMs;
- newEndMs = nowMs;
- }
-
- // Phase 2 only: use preset so GetMinMax produces "last X from now".
- // Phase 1: preset=null so the center-anchored range is preserved (GetMinMax would discard it).
- const preset = isPhase1 ? null : PRESET_FOR_DURATION_MS[newDurationMs] ?? null;
-
- return {
- range: [Math.round(newStartMs), Math.round(newEndMs)],
- preset,
- };
-}
diff --git a/frontend/src/utils/metricsTimeStorageUtils.ts b/frontend/src/utils/metricsTimeStorageUtils.ts
deleted file mode 100644
index 77579a7c0c..0000000000
--- a/frontend/src/utils/metricsTimeStorageUtils.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import getLocalStorageKey from 'api/browser/localstorage/get';
-import setLocalStorageKey from 'api/browser/localstorage/set';
-import { LOCALSTORAGE } from 'constants/localStorage';
-
-/**
- * Updates the stored time duration for a route in localStorage.
- * Used by both DateTimeSelectionV2 (manual time pick) and useZoomOut (zoom out button).
- *
- * @param pathname - The route path (e.g. /infrastructure-monitoring/hosts)
- * @param value - The time value to store (preset string like '1w' or JSON string for custom range)
- */
-export function persistTimeDurationForRoute(
- pathname: string,
- value: string,
-): void {
- const preRoutes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
- let preRoutesObject: Record = {};
- try {
- preRoutesObject = preRoutes ? JSON.parse(preRoutes) : {};
- } catch {
- preRoutesObject = {};
- }
- const preRoute = { ...preRoutesObject, [pathname]: value };
- setLocalStorageKey(
- LOCALSTORAGE.METRICS_TIME_IN_DURATION,
- JSON.stringify(preRoute),
- );
-}