Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
66 changes: 48 additions & 18 deletions static/app/components/pageFilters/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ export type InitializeUrlStateParams = {
organization: Organization;
defaultSelection?: Partial<PageFilters>;
forceProject?: MinimalProject | null;
/**
* the maximum number of sequential days that can be selected on the date page filter
*/
maxDateRange?: number;
/**
* When set, the stats period will fallback to the `maxPickableDays` days if the stored selection exceeds the limit.
*/
Expand Down Expand Up @@ -157,6 +161,7 @@ export function initializeUrlState({
skipLoadLastUsed,
skipLoadLastUsedEnvironment,
maxPickableDays,
maxDateRange,
shouldPersist = true,
shouldForceProject,
defaultSelection,
Expand Down Expand Up @@ -308,6 +313,7 @@ export function initializeUrlState({
}

let shouldUseMaxPickableDays = false;
let shouldUseMaxDateRange = false;

if (maxPickableDays && pageFilters.datetime) {
let {start, end} = pageFilters.datetime;
Expand All @@ -320,16 +326,33 @@ export function initializeUrlState({

if (start && end) {
const periodStart = new Date(start);
const periodEnd = new Date(end);
const maxPeriod = parseStatsPeriod(`${maxPickableDays}d`);
const maxTimeRange = (maxDateRange ?? maxPickableDays) * 24 * 60 * 60 * 1000;
const maxStart = new Date(maxPeriod.start);
if (periodStart.getTime() < maxStart.getTime()) {
shouldUseMaxPickableDays = true;
pageFilters.datetime = {
period: `${maxPickableDays}d`,
start: null,
end: null,
utc: datetime.utc,
};
if (maxDateRange) {
if (
periodEnd.getTime() - periodStart.getTime() > maxTimeRange ||
periodStart.getTime() < maxStart.getTime()
) {
shouldUseMaxDateRange = true;
pageFilters.datetime = {
period: `${maxDateRange}d`,
start: null,
end: null,
utc: datetime.utc,
};
}
} else {
if (periodStart.getTime() < maxStart.getTime()) {
shouldUseMaxPickableDays = true;
pageFilters.datetime = {
period: `${maxPickableDays}d`,
start: null,
end: null,
utc: datetime.utc,
};
}
}
}
}
Expand All @@ -343,21 +366,28 @@ export function initializeUrlState({
);
}

const newDatetime = shouldUseMaxPickableDays
const newDatetime = shouldUseMaxDateRange
? {
period: `${maxPickableDays}d`,
period: `${maxDateRange}d`,
start: null,
end: null,
utc: datetime.utc,
}
: {
...datetime,
period:
parsed.start || parsed.end || parsed.period || shouldUsePinnedDatetime
? datetime.period
: null,
utc: parsed.utc || shouldUsePinnedDatetime ? datetime.utc : null,
};
: shouldUseMaxPickableDays
? {
period: `${maxPickableDays}d`,
start: null,
end: null,
utc: datetime.utc,
}
: {
...datetime,
period:
parsed.start || parsed.end || parsed.period || shouldUsePinnedDatetime
? datetime.period
: null,
utc: parsed.utc || shouldUsePinnedDatetime ? datetime.utc : null,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Instead of a nested ternary like this, could we use an if block, imo it's a tad easier to read


if (!skipInitializeUrlParams) {
updateParams({project, environment, ...newDatetime}, location, navigate, {
Expand Down
99 changes: 99 additions & 0 deletions static/app/components/pageFilters/container.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,105 @@ describe('PageFiltersContainer', () => {
});
});

describe('maxDateRange param', () => {
it('resets period when maxDateRange appears and current selection exceeds it', async () => {
const {rerender} = render(<PageFiltersContainer maxPickableDays={30} />, {
organization,
initialRouterConfig: {
location: {
pathname: '/organizations/org-slug/test/',
query: {statsPeriod: '14d'},
},
route: '/organizations/:orgId/test/',
},
});

await waitFor(() =>
expect(PageFiltersStore.getState().selection.datetime).toEqual({
period: '14d',
utc: null,
start: null,
end: null,
})
);

rerender(<PageFiltersContainer maxPickableDays={30} maxDateRange={7} />);

await waitFor(() =>
expect(PageFiltersStore.getState().selection.datetime).toEqual({
period: '7d',
utc: null,
start: null,
end: null,
})
);
});

it('does not reset period when maxDateRange appears but selection is within it', async () => {
const {rerender} = render(<PageFiltersContainer maxPickableDays={30} />, {
organization,
initialRouterConfig: {
location: {
pathname: '/organizations/org-slug/test/',
query: {statsPeriod: '7d'},
},
route: '/organizations/:orgId/test/',
},
});

await waitFor(() =>
expect(PageFiltersStore.getState().selection.datetime).toEqual({
period: '7d',
utc: null,
start: null,
end: null,
})
);

rerender(<PageFiltersContainer maxPickableDays={30} maxDateRange={14} />);

await waitFor(() =>
expect(PageFiltersStore.getState().selection.datetime).toEqual({
period: '7d',
utc: null,
start: null,
end: null,
})
);
});

it('resets absolute range when maxDateRange appears and range exceeds it', async () => {
const start = moment().subtract(10, 'days').format('YYYY-MM-DDTHH:mm:ss');
const end = moment().subtract(1, 'days').format('YYYY-MM-DDTHH:mm:ss');

const {rerender} = render(<PageFiltersContainer maxPickableDays={30} />, {
organization,
initialRouterConfig: {
location: {
pathname: '/organizations/org-slug/test/',
query: {start, end},
},
route: '/organizations/:orgId/test/',
},
});

await waitFor(() =>
expect(PageFiltersStore.getState().selection.datetime.period).toBeNull()
);

rerender(<PageFiltersContainer maxPickableDays={30} maxDateRange={7} />);

await waitFor(() =>
expect(PageFiltersStore.getState().selection.datetime).toEqual({
period: '7d',
utc: null,
start: null,
end: null,
})
);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should a test be added in checking that absolute date ranges don't get reset as well, just a small regression test 👀

describe('skipInitializeUrlParams', () => {
const skipInitProjects = [
ProjectFixture({id: '1', slug: 'staging-project', environments: ['staging']}),
Expand Down
44 changes: 35 additions & 9 deletions static/app/components/pageFilters/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function PageFiltersContainer({
skipLoadLastUsed,
skipLoadLastUsedEnvironment,
maxPickableDays,
maxDateRange,
children,
...props
}: Props) {
Expand Down Expand Up @@ -103,6 +104,7 @@ export function PageFiltersContainer({
skipLoadLastUsed,
skipLoadLastUsedEnvironment,
maxPickableDays,
maxDateRange,
memberProjects,
nonMemberProjects,
defaultSelection,
Expand Down Expand Up @@ -132,20 +134,24 @@ export function PageFiltersContainer({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectsLoaded]);

// Handle dynamic maxPickableDays changes (e.g., switching between pages with different limits).
// Handle dynamic maxPickableDays/maxDateRange changes (e.g., switching between pages with different limits).
// When the limit decreases and the current selection exceeds it, reset to the new max.
const previousMaxPickableDays = usePrevious(maxPickableDays);
const previousMaxDateRange = usePrevious(maxDateRange);
const shouldResetDateTime = useMemo(() => {
// Don't act until page filters are initialized - selection.datetime contains
// default values until isReady, not the actual URL state
if (!isReady) {
return false;
}

// Only act when maxPickableDays decreases (increasing the limit never invalidates selection)
const effectiveMaxDays = maxDateRange ?? maxPickableDays;
const previousEffectiveMaxDays = previousMaxDateRange ?? previousMaxPickableDays;

// Only act when the effective limit decreases (increasing the limit never invalidates selection)
if (
previousMaxPickableDays === maxPickableDays ||
previousMaxPickableDays < maxPickableDays
previousEffectiveMaxDays === effectiveMaxDays ||
previousEffectiveMaxDays < effectiveMaxDays
) {
return false;
}
Expand All @@ -154,19 +160,39 @@ export function PageFiltersContainer({

// For relative periods (e.g., "14d"), check if the period exceeds the new max
if (period) {
return statsPeriodToDays(period) > maxPickableDays;
Comment thread
nikkikapadia marked this conversation as resolved.
return statsPeriodToDays(period) > effectiveMaxDays;
}

// For absolute date ranges, check if the start date is before the allowed window.
// Uses same calculation as initialization in pageFilters.tsx
if (start && end) {
const periodStart = new Date(start);
const periodEnd = new Date(end);
const maxPeriod = parseStatsPeriod(`${maxPickableDays}d`);
const maxStart = new Date(maxPeriod.start);
return new Date(start).getTime() < maxStart.getTime();

if (maxDateRange) {
const maxTimeRange = maxDateRange * 24 * 60 * 60 * 1000;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a shared const somewhere in the FE for one day? Wondering if it's worth pulling that out just to be consistent and clearer what we're doing rather than having the raw numbers 🤔

return (
periodEnd.getTime() - periodStart.getTime() > maxTimeRange ||
periodStart.getTime() < maxStart.getTime()
);
}

return periodStart.getTime() < maxStart.getTime();
}

return false;
}, [isReady, maxPickableDays, previousMaxPickableDays, selection.datetime]);
}, [
isReady,
maxDateRange,
maxPickableDays,
previousMaxDateRange,
previousMaxPickableDays,
selection.datetime,
]);

const resetPeriodDays = maxDateRange ?? maxPickableDays;

useLayoutEffect(() => {
if (!shouldResetDateTime) {
Comment thread
sentry[bot] marked this conversation as resolved.
Expand All @@ -175,15 +201,15 @@ export function PageFiltersContainer({

// Reset to a relative period matching the new max (clears any absolute dates)
const newDateState = getDatetimeFromState({
period: `${maxPickableDays}d`,
period: `${resetPeriodDays}d`,
start: null,
end: null,
utc: selection.datetime.utc,
environment: [],
project: [],
});
updateDateTime(newDateState, location, navigate);
}, [maxPickableDays, location, navigate, selection.datetime.utc, shouldResetDateTime]);
}, [location, navigate, resetPeriodDays, selection.datetime.utc, shouldResetDateTime]);

// Update store persistence when `disablePersistence` changes
useEffect(() => updatePersistence(!disablePersistence), [disablePersistence]);
Expand Down
9 changes: 7 additions & 2 deletions static/app/utils/useDatePageFilterProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function useDatePageFilterProps({
defaultPeriod,
maxPickableDays,
maxUpgradableDays,
maxDateRange,
upsellFooter,
}: UseDatePageFilterPropsProps): DatePageFilterProps {
return useMemo(() => {
Expand All @@ -26,9 +27,12 @@ export function useDatePageFilterProps({
[90, '90d', t('Last 90 days')],
];

// if maxDateRange is set, we need to make sure the options shown don't exceed this max range.
// find the relative options that should be enabled based on the maxPickableDays
const pickableIndex =
availableRelativeOptions.findLastIndex(([days]) => days <= maxPickableDays) + 1;
availableRelativeOptions.findLastIndex(([days]) =>
maxDateRange ? days <= maxDateRange : days <= maxPickableDays
) + 1;
const enabledOptions = Object.fromEntries(
availableRelativeOptions
.slice(0, pickableIndex)
Expand All @@ -54,12 +58,13 @@ export function useDatePageFilterProps({
defaultPeriod,
isOptionDisabled,
maxPickableDays,
maxDateRange,
menuFooter,
relativeOptions: ({arbitraryOptions}) => ({
...arbitraryOptions,
...enabledOptions,
...disabledOptions,
}),
};
}, [defaultPeriod, maxPickableDays, maxUpgradableDays, upsellFooter]);
}, [defaultPeriod, maxDateRange, maxPickableDays, maxUpgradableDays, upsellFooter]);
}
4 changes: 4 additions & 0 deletions static/app/utils/useMaxPickableDays.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export interface MaxPickableDaysOptions {
*/
maxUpgradableDays: NonNullable<DatePageFilterProps['maxPickableDays']>;
defaultPeriod?: DatePageFilterProps['defaultPeriod'];
/**
* The maximum number of sequential days that can be selected on the date page filter
*/
maxDateRange?: number;
upsellFooter?: ReactNode;
}

Expand Down
2 changes: 1 addition & 1 deletion static/app/views/explore/spans/content.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ describe('ExploreContent', () => {
).toBeInTheDocument();
});

it('resets period when max pickable days decreases', async () => {
it('resets period when maxDateRange is applied after cross events are added', async () => {
PageFiltersStore.onInitializeUrlState({
projects: [project].map(p => parseInt(p.id, 10)),
environments: [],
Expand Down
Loading
Loading