diff --git a/static/app/utils/useChartInterval.spec.tsx b/static/app/utils/useChartInterval.spec.tsx index 69601f362e51..1ee2c7221ce8 100644 --- a/static/app/utils/useChartInterval.spec.tsx +++ b/static/app/utils/useChartInterval.spec.tsx @@ -137,6 +137,39 @@ describe('useChartInterval', () => { expect(chartInterval).toBe('3h'); }); + it('offers a 30 day interval for long ranges when requested', () => { + let chartInterval!: ReturnType[0]; + let intervalOptions!: ReturnType[2]; + let defaultIntervalOptions!: ReturnType[2]; + + function TestPage() { + // oxlint-disable-next-line react/globals -- Test captures the hook result in an outer variable to assert on it. + [chartInterval, , intervalOptions] = useChartInterval({ + unspecifiedStrategy: ChartIntervalUnspecifiedStrategy.USE_BIGGEST, + includeThirtyDayInterval: true, + }); + // oxlint-disable-next-line react/globals -- Test captures the hook result in an outer variable to assert on it. + [, , defaultIntervalOptions] = useChartInterval(); + return null; + } + + render(); + expect(intervalOptions.map(o => o.value)).not.toContain('30d'); + + act(() => + PageFiltersStore.updateDateTime({ + period: '90d', + start: null, + end: null, + utc: true, + }) + ); + + expect(intervalOptions).toContainEqual({value: '30d', label: '30 days'}); + expect(chartInterval).not.toBe('30d'); + expect(defaultIntervalOptions.map(o => o.value)).not.toContain('30d'); + }); + it('preserves a custom interval that is not too granular for the time range', () => { // Default 14d period produces ladder-derived options ['1h', '3h', '6h', '1d']. // A 12h interval is coarser than the minimum (1h), so it should be preserved diff --git a/static/app/utils/useChartInterval.tsx b/static/app/utils/useChartInterval.tsx index 6e801ba932fa..59051ef4d5ef 100644 --- a/static/app/utils/useChartInterval.tsx +++ b/static/app/utils/useChartInterval.tsx @@ -9,6 +9,7 @@ import { GranularityLadder, ONE_HOUR, SIX_HOURS, + SIXTY_DAYS, THIRTY_DAYS, TWELVE_HOURS, TWO_WEEKS, @@ -31,7 +32,14 @@ export enum ChartIntervalUnspecifiedStrategy { export function useChartInterval({ unspecifiedStrategy = ChartIntervalUnspecifiedStrategy.USE_SMALLEST, -}: {unspecifiedStrategy?: ChartIntervalUnspecifiedStrategy} = {}): [ + includeThirtyDayInterval = false, +}: { + /** + * Offer a 30 day interval for long time ranges. Never used as the default. + */ + includeThirtyDayInterval?: boolean; + unspecifiedStrategy?: ChartIntervalUnspecifiedStrategy; +} = {}): [ string, (interval: string) => void, intervalOptions: Array<{label: string; value: string}>, @@ -64,6 +72,13 @@ export function useChartInterval({ options.push(ONE_DAY_OPTION); } + if ( + includeThirtyDayInterval && + diffInMinutes >= MINIMUM_DURATION_FOR_THIRTY_DAY_INTERVAL + ) { + options.push(THIRTY_DAY_OPTION); + } + return { baseIntervalOptions: options, defaultInterval: fallback, @@ -76,7 +91,7 @@ export function useChartInterval({ // this would produce a single (incomplete) bucket. timeRangeInHours: diffInMinutes / 60, }; - }, [datetime, unspecifiedStrategy]); + }, [datetime, unspecifiedStrategy, includeThirtyDayInterval]); const [interval, intervalOptions] = useMemo((): [ string, @@ -168,6 +183,9 @@ const MAXIMUM_INTERVAL = new GranularityLadder([ const ONE_DAY_OPTION = {value: '1d', label: t('1 day')}; const MINIMUM_DURATION_FOR_ONE_DAY_INTERVAL = TWO_WEEKS; +const THIRTY_DAY_OPTION = {value: '30d', label: t('30 days')}; +const MINIMUM_DURATION_FOR_THIRTY_DAY_INTERVAL = SIXTY_DAYS; + /** * Builds a human readable label for an arbitrary interval shorthand (e.g. `1d`), * preserving the unit chosen rather than normalizing it. diff --git a/static/app/views/dashboards/hooks/useDashboardChartInterval.tsx b/static/app/views/dashboards/hooks/useDashboardChartInterval.tsx index d147f9a53041..318e50210187 100644 --- a/static/app/views/dashboards/hooks/useDashboardChartInterval.tsx +++ b/static/app/views/dashboards/hooks/useDashboardChartInterval.tsx @@ -5,11 +5,13 @@ import { /** * Wrapper around `useChartInterval` that uses the `USE_SECOND_BIGGEST` - * strategy for dashboards. This keeps the dashboard detail page, widget - * builder preview, and filters bar in sync. + * strategy for dashboards and offers a 30 day interval on long ranges. This + * keeps the dashboard detail page, widget builder preview, and filters bar in + * sync. */ export function useDashboardChartInterval() { return useChartInterval({ unspecifiedStrategy: ChartIntervalUnspecifiedStrategy.USE_SECOND_BIGGEST, + includeThirtyDayInterval: true, }); }