Skip to content
Draft
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
2 changes: 2 additions & 0 deletions static/app/components/charts/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ export const SIXTY_DAYS = 86400;
export const THIRTY_DAYS = 43200;
export const TWO_WEEKS = 20160;
export const ONE_WEEK = 10080;
export const FOUR_DAYS = 5760;
export const FORTY_EIGHT_HOURS = 2880;
export const TWENTY_FOUR_HOURS = 1440;
export const TWELVE_HOURS = 720;
export const SIX_HOURS = 360;
const THREE_HOURS = 180;
export const ONE_HOUR = 60;
Expand Down
8 changes: 3 additions & 5 deletions static/app/utils/useChartInterval.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ describe('useChartInterval', () => {
expect(intervalOptions).toEqual([
{value: '1h', label: '1 hour'},
{value: '3h', label: '3 hours'},
{value: '12h', label: '12 hours'},
{value: '1d', label: '1 day'},
{value: '6h', label: '6 hours'},
]);
expect(chartInterval).toBe('1h'); // default

Expand All @@ -47,12 +46,11 @@ describe('useChartInterval', () => {
expect(intervalOptions).toEqual([
{value: '1m', label: '1 minute'},
{value: '5m', label: '5 minutes'},
{value: '15m', label: '15 minutes'},
]);
act(() => {
setChartInterval('15m');
setChartInterval('5m');
});
expect(chartInterval).toBe('15m');
expect(chartInterval).toBe('5m');
});
});

Expand Down
29 changes: 16 additions & 13 deletions static/app/utils/useChartInterval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import type {Location} from 'history';
import {
FIVE_MINUTES,
FORTY_EIGHT_HOURS,
FOUR_DAYS,
getDiffInMinutes,
GranularityLadder,
ONE_HOUR,
ONE_WEEK,
SIX_HOURS,
THIRTY_DAYS,
TWELVE_HOURS,
TWO_WEEKS,
} from 'sentry/components/charts/utils';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
Expand All @@ -20,7 +21,7 @@ import {decodeScalar} from 'sentry/utils/queryString';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate} from 'sentry/utils/useNavigate';

enum ChartIntervalUnspecifiedStrategy {
export enum ChartIntervalUnspecifiedStrategy {
/** Use the second biggest possible interval (e.g., pretty big buckets) */
USE_SECOND_BIGGEST = 'use_second_biggest',
/** Use the smallest possible interval (e.g., the smallest possible buckets) */
Expand Down Expand Up @@ -104,12 +105,12 @@ function useChartIntervalImpl({
const ALL_INTERVAL_OPTIONS = [
{value: '1m', label: t('1 minute')},
{value: '5m', label: t('5 minutes')},
{value: '15m', label: t('15 minutes')},
{value: '10m', label: t('10 minutes')},
{value: '30m', label: t('30 minutes')},
{value: '1h', label: t('1 hour')},
{value: '3h', label: t('3 hours')},
{value: '6h', label: t('6 hours')},
{value: '12h', label: t('12 hours')},
{value: '1d', label: t('1 day')},
];

/**
Expand All @@ -119,19 +120,21 @@ const ALL_INTERVAL_OPTIONS = [
const MINIMUM_INTERVAL = new GranularityLadder([
[THIRTY_DAYS, '3h'],
[TWO_WEEKS, '1h'],
[ONE_WEEK, '30m'],
[FORTY_EIGHT_HOURS, '15m'],
[SIX_HOURS, '5m'],
[FOUR_DAYS, '30m'],
[FORTY_EIGHT_HOURS, '10m'],
[TWELVE_HOURS, '5m'],
[SIX_HOURS, '1m'],
[0, '1m'],
]);

const MAXIMUM_INTERVAL = new GranularityLadder([
[THIRTY_DAYS, '1d'],
[TWO_WEEKS, '1d'],
[ONE_WEEK, '12h'],
[FORTY_EIGHT_HOURS, '4h'],
[SIX_HOURS, '1h'],
[ONE_HOUR, '15m'],
[THIRTY_DAYS, '12h'],
[TWO_WEEKS, '6h'],
[FOUR_DAYS, '3h'],
[FORTY_EIGHT_HOURS, '1h'],
[TWELVE_HOURS, '30m'],
[SIX_HOURS, '10m'],
[ONE_HOUR, '5m'],
[FIVE_MINUTES, '5m'],
[0, '1m'],
]);
Expand Down
28 changes: 14 additions & 14 deletions static/app/views/dashboards/dashboard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -548,11 +548,11 @@ describe('Dashboards > Dashboard', () => {
body: [],
match: [MockApiClient.matchQuery({interval: '5m'})],
});
const hourlyIntervalMock = MockApiClient.addMockResponse({
const thirtyMinuteMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-stats/',
method: 'GET',
body: [],
match: [MockApiClient.matchQuery({interval: '1h'})],
match: [MockApiClient.matchQuery({interval: '30m'})],
});

// No interval in the URL — the 5m default is derived purely from the
Expand All @@ -564,16 +564,16 @@ describe('Dashboards > Dashboard', () => {

await screen.findByText('Test Spans Widget');
await waitFor(() => expect(fiveMinuteMock).toHaveBeenCalled());
expect(hourlyIntervalMock).not.toHaveBeenCalled();
expect(thirtyMinuteMock).not.toHaveBeenCalled();

// Click the interval selector and choose '1 hour'. FiltersBar writes
// interval=1h to the URL, DashboardWithIntervalSelector re-renders with
// Click the interval selector and choose '30 minutes'. FiltersBar writes
// interval=30m to the URL, DashboardWithIntervalSelector re-renders with
// the new widgetInterval, and the widget re-fetches with the new interval.
await userEvent.click(screen.getByRole('button', {name: '5 minutes'}));
await userEvent.click(screen.getByRole('option', {name: '1 hour'}));
await userEvent.click(screen.getByRole('option', {name: '30 minutes'}));

await waitFor(() => expect(hourlyIntervalMock).toHaveBeenCalled());
expect(router.location.query.interval).toBe('1h');
await waitFor(() => expect(thirtyMinuteMock).toHaveBeenCalled());
expect(router.location.query.interval).toBe('30m');
});
});

Expand All @@ -592,11 +592,11 @@ describe('Dashboards > Dashboard', () => {
match: [MockApiClient.matchQuery({interval: '5m'})],
});

const hourlyIntervalMock = MockApiClient.addMockResponse({
const tenMinuteMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-stats/',
method: 'GET',
body: [],
match: [MockApiClient.matchQuery({interval: '1h'})],
match: [MockApiClient.matchQuery({interval: '10m'})],
});

const {router} = render(<DashboardWithIntervalSelector />, {
Expand All @@ -615,16 +615,16 @@ describe('Dashboards > Dashboard', () => {

// Selecting a new interval updates the URL and triggers a re-fetch.
await userEvent.click(screen.getByRole('button', {name: '30 minutes'}));
await userEvent.click(screen.getByRole('option', {name: '1 hour'}));
await userEvent.click(screen.getByRole('option', {name: '10 minutes'}));

await waitFor(() => expect(hourlyIntervalMock).toHaveBeenCalled());
expect(router.location.query.interval).toBe('1h');
await waitFor(() => expect(tenMinuteMock).toHaveBeenCalled());
expect(router.location.query.interval).toBe('10m');
});
});

describe('URL interval not valid for the dashboard period', () => {
beforeEach(() => {
// Override the outer 24h store setup — valid intervals for 30d are 3h, 12h, 1d.
// Override the outer 24h store setup — valid intervals for 30d are 3h, 6h, 12h.
PageFiltersStore.init();
PageFiltersStore.onInitializeUrlState(
getSavedFiltersAsPageFilters(thirtyDayDashboard)
Expand Down
9 changes: 7 additions & 2 deletions static/app/views/dashboards/detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ import {OnRouteLeave} from 'sentry/utils/reactRouter6Compat/onRouteLeave';
import {scheduleMicroTask} from 'sentry/utils/scheduleMicroTask';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
import {useApi} from 'sentry/utils/useApi';
import {useChartInterval} from 'sentry/utils/useChartInterval';
import {
ChartIntervalUnspecifiedStrategy,
useChartInterval,
} from 'sentry/utils/useChartInterval';
import {useLocation} from 'sentry/utils/useLocation';
import type {ReactRouter3Navigate} from 'sentry/utils/useNavigate';
import {useNavigate} from 'sentry/utils/useNavigate';
Expand Down Expand Up @@ -1462,7 +1465,9 @@ export function DashboardDetailWithInjectedProps(
const location = useLocation();
const params = useParams<RouteParams>();
const router = useRouter();
const [chartInterval] = useChartInterval();
const [chartInterval] = useChartInterval({
unspecifiedStrategy: ChartIntervalUnspecifiedStrategy.USE_SECOND_BIGGEST,
});
const queryClient = useQueryClient();
// Always use the validated chart interval so the UI dropdown and widget
// requests stay in sync. chartInterval is validated against the current page
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {useState} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {dedupeArray} from 'sentry/utils/dedupeArray';
import type {Sort} from 'sentry/utils/discover/fields';
import {useChartInterval} from 'sentry/utils/useChartInterval';
import {
ChartIntervalUnspecifiedStrategy,
useChartInterval,
} from 'sentry/utils/useChartInterval';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate} from 'sentry/utils/useNavigate';
import {useOrganization} from 'sentry/utils/useOrganization';
Expand Down Expand Up @@ -44,7 +47,9 @@ export function WidgetPreview({
const location = useLocation();
const navigate = useNavigate();
const pageFilters = usePageFilters();
const [chartInterval] = useChartInterval();
const [chartInterval] = useChartInterval({
unspecifiedStrategy: ChartIntervalUnspecifiedStrategy.USE_SECOND_BIGGEST,
});

const {state, dispatch} = useWidgetBuilderContext();
const [tableWidths, setTableWidths] = useState<number[]>();
Expand Down
Loading