From 5ceaa60d2ec25e6a575538ca8749f73927910c55 Mon Sep 17 00:00:00 2001 From: Shaun Kaasten <900809+skaasten@users.noreply.github.com> Date: Mon, 25 May 2026 09:38:16 -0400 Subject: [PATCH 1/5] fix(dashboards): Anchor Editors dropdown to the right edge of the trigger (#116104) Fix editor dropdown placement, so the apply button is visible Before image After image Fixes DAIN-1677 --- static/app/views/dashboards/editAccessSelector.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/app/views/dashboards/editAccessSelector.tsx b/static/app/views/dashboards/editAccessSelector.tsx index 214e40fcc50e..44695afbb6f4 100644 --- a/static/app/views/dashboards/editAccessSelector.tsx +++ b/static/app/views/dashboards/editAccessSelector.tsx @@ -361,6 +361,7 @@ export function EditAccessSelector({ /> } + position="bottom-end" strategy="fixed" preventOverflowOptions={{mainAxis: false}} disabled={disabled} From 84065b8b91e9977527cc6392c254947e28f8b215 Mon Sep 17 00:00:00 2001 From: Shaun Kaasten <900809+skaasten@users.noreply.github.com> Date: Mon, 25 May 2026 10:03:16 -0400 Subject: [PATCH 2/5] fix(dashboards): Stop widget header action clicks from bubbling (#116096) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop click events from bubbling out of widget header action buttons (full-screen view, copy URL). On prebuilt dashboards like Web Vitals that wire a slideout drawer to the wrapper's `onClick`, the bubble caused the full-screen modal and the slideout drawer to open from a single click and overlap. **Root cause** The header buttons in `widgetFrame.tsx` didn't `e.stopPropagation()`, so a click on the expand icon fired the button's `onFullScreenViewClick` *and* bubbled up to `GridWidgetWrapper`'s `onClick` in `sortableWidget.tsx`, which opens the slideout. Same bubble exists for the Copy URL button and any single custom action. **Fix** Each header action button now stops propagation before invoking its handler. The wrapper's click target stays the widget body, so the slideout only opens when the user actually clicks into the widget body — not when they interact with a header button. Fixes DAIN-1684 --- .../views/dashboards/widgetCard/widgetFrame.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/static/app/views/dashboards/widgetCard/widgetFrame.tsx b/static/app/views/dashboards/widgetCard/widgetFrame.tsx index 6a0afac14447..11e571562f37 100644 --- a/static/app/views/dashboards/widgetCard/widgetFrame.tsx +++ b/static/app/views/dashboards/widgetCard/widgetFrame.tsx @@ -108,7 +108,10 @@ export function WidgetFrame(props: WidgetFrameProps) { { + e.stopPropagation(); + actions[0]!.onAction?.(); + }} to={actions[0]!.to} > {actions[0]!.label} @@ -117,7 +120,10 @@ export function WidgetFrame(props: WidgetFrameProps) { @@ -148,7 +154,8 @@ export function WidgetFrame(props: WidgetFrameProps) { aria-label={t('Copy Widget URL')} variant="transparent" icon={} - onClick={() => { + onClick={e => { + e.stopPropagation(); props.onCopyUrlClick?.(); }} /> @@ -161,7 +168,8 @@ export function WidgetFrame(props: WidgetFrameProps) { aria-label={t('Open Full-Screen View')} variant="transparent" icon={} - onClick={() => { + onClick={e => { + e.stopPropagation(); props.onFullScreenViewClick?.(); }} /> From 0b8586e07eae8a299bba7a32704a14393c479cc0 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 25 May 2026 11:05:05 -0300 Subject: [PATCH 3/5] fix(cross-events): Correct styling based off date selection (#116124) The goal of this PR is to tweak the styles for the cross-event query section based off of the period selection. There are a couple issues currently when selecting an exact date range so this PR tweaks the styles based off of that condition. --- .../crossEventMetricsSearchBar.tsx | 32 ++++++++------ .../crossEvents/crossEventSearchBars.tsx | 25 +++++++++-- .../explore/spans/spansTabSearchSection.tsx | 43 +++++++++++++------ 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/static/app/views/explore/spans/crossEvents/crossEventMetricsSearchBar.tsx b/static/app/views/explore/spans/crossEvents/crossEventMetricsSearchBar.tsx index 7f37496af3b8..e05dc73cac1c 100644 --- a/static/app/views/explore/spans/crossEvents/crossEventMetricsSearchBar.tsx +++ b/static/app/views/explore/spans/crossEvents/crossEventMetricsSearchBar.tsx @@ -1,6 +1,6 @@ import {memo, useCallback, useMemo} from 'react'; -import {Grid} from '@sentry/scraps/layout'; +import {Container, Grid} from '@sentry/scraps/layout'; import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/context'; import { @@ -143,19 +143,23 @@ export const SpansTabCrossEventMetricsSearchBar = memo( }); return ( - - - - - + + + + + + + + + ); } diff --git a/static/app/views/explore/spans/crossEvents/crossEventSearchBars.tsx b/static/app/views/explore/spans/crossEvents/crossEventSearchBars.tsx index e852318d7cae..e7194c61586c 100644 --- a/static/app/views/explore/spans/crossEvents/crossEventSearchBars.tsx +++ b/static/app/views/explore/spans/crossEvents/crossEventSearchBars.tsx @@ -2,7 +2,7 @@ import {Fragment, useEffect, useEffectEvent} from 'react'; import {Button} from '@sentry/scraps/button'; import {CompactSelect} from '@sentry/scraps/compactSelect'; -import {Container} from '@sentry/scraps/layout'; +import {Container, Grid} from '@sentry/scraps/layout'; import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; @@ -30,7 +30,13 @@ import {TraceItemDataset} from 'sentry/views/explore/types'; const EMPTY_CROSS_EVENTS: CrossEvent[] = []; -export function SpansTabCrossEventSearchBars() { +interface SpansTabCrossEventSearchBarsProps { + hasIndependentDateColumn?: boolean; +} + +export function SpansTabCrossEventSearchBars({ + hasIndependentDateColumn = false, +}: SpansTabCrossEventSearchBarsProps) { const organization = useOrganization(); const crossEvents = useQueryParamsCrossEvents() ?? EMPTY_CROSS_EVENTS; const setCrossEvents = useSetQueryParamsCrossEvents(); @@ -71,7 +77,7 @@ export function SpansTabCrossEventSearchBars() { return null; } - return visibleCrossEvents.map(({crossEvent, index}, visibleIndex) => { + const crossEventRows = visibleCrossEvents.map(({crossEvent, index}, visibleIndex) => { let traceItemType = TraceItemDataset.SPANS; if (crossEvent.type === 'logs') { traceItemType = TraceItemDataset.LOGS; @@ -176,4 +182,17 @@ export function SpansTabCrossEventSearchBars() { ); }); + + if (!hasIndependentDateColumn) { + return {crossEventRows}; + } + + return ( + + {crossEventRows} + + ); } diff --git a/static/app/views/explore/spans/spansTabSearchSection.tsx b/static/app/views/explore/spans/spansTabSearchSection.tsx index ac7e2979817e..480c47c2bc68 100644 --- a/static/app/views/explore/spans/spansTabSearchSection.tsx +++ b/static/app/views/explore/spans/spansTabSearchSection.tsx @@ -9,6 +9,7 @@ import {DatePageFilter} from 'sentry/components/pageFilters/date/datePageFilter' import {EnvironmentPageFilter} from 'sentry/components/pageFilters/environment/environmentPageFilter'; import {PageFilterBar} from 'sentry/components/pageFilters/pageFilterBar'; import {ProjectPageFilter} from 'sentry/components/pageFilters/project/projectPageFilter'; +import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {useSpanSearchQueryBuilderProps} from 'sentry/components/performance/spanSearchQueryBuilder'; import { SearchQueryBuilderProvider, @@ -73,6 +74,7 @@ export function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSection const crossEvents = useQueryParamsCrossEvents(); const setQueryParams = useSetQueryParams(); const [caseInsensitive, setCaseInsensitive] = useCaseInsensitivity(); + const {selection} = usePageFilters(); const organization = useOrganization(); const hasRawSearchReplacement = organization.features.includes( @@ -80,6 +82,9 @@ export function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSection ); const hasCrossEvents = defined(crossEvents) && crossEvents.length > 0; + const hasAbsoluteDateSelection = Boolean( + selection.datetime.start && selection.datetime.end && !selection.datetime.period + ); const {attributes: numberAttributes, isLoading: numberAttributesLoading} = useSpanItemAttributes({}, 'number'); @@ -167,20 +172,30 @@ export function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSection > {tourProps => (
- - - - - - - - - {hasCrossEvents ? : null} + + + + + + + + + + {hasCrossEvents && !hasAbsoluteDateSelection ? ( + + ) : null} + + {hasCrossEvents && hasAbsoluteDateSelection ? ( + + ) : null} {hasCrossEvents ? null : ( From 3eb788aac2ceeb6a9467d4712a72c48a42cb31ec Mon Sep 17 00:00:00 2001 From: Nikki Kapadia <72356613+nikkikapadia@users.noreply.github.com> Date: Mon, 25 May 2026 10:18:21 -0400 Subject: [PATCH 4/5] fix(explore): cross events date selector allow 7d anytime within 30 days (#116099) For cross event queries in explore we only limit the user to making 7 day queries at a time. The only issue was we needed to have the ability to query a 7 day range within the `maxPickableDays` (of 90 days in this case) but there's no way to do that right now since `maxPickableDays` doesn't respect `maxDateRange`. Ideally if there's a `maxDateRange` the default time periods shown will be within that date range instead of just relying on `maxPickableDays`. I've only made changes to the logic if BOTH `maxPickableDays` and `maxDateRange` are passed in. I took a look to see if there are any other parts of the app that use `maxDateRange` and i don't see any references to it so this shouldn't cause odd behaviour on any other date selector :) Here's what it's looking like: https://github.com/user-attachments/assets/eedea627-5955-4c16-addf-f31c1cc0e4f2 --- static/app/components/pageFilters/actions.tsx | 79 +++++++---- .../components/pageFilters/container.spec.tsx | 124 ++++++++++++++++++ .../app/components/pageFilters/container.tsx | 45 +++++-- .../components/timeRangeSelector/utils.tsx | 2 +- static/app/utils/useDatePageFilterProps.tsx | 9 +- static/app/utils/useMaxPickableDays.tsx | 4 + .../app/views/explore/spans/content.spec.tsx | 2 +- static/app/views/explore/spans/content.tsx | 18 ++- 8 files changed, 240 insertions(+), 43 deletions(-) diff --git a/static/app/components/pageFilters/actions.tsx b/static/app/components/pageFilters/actions.tsx index a9e1f369f640..efb3d77fdc48 100644 --- a/static/app/components/pageFilters/actions.tsx +++ b/static/app/components/pageFilters/actions.tsx @@ -110,6 +110,10 @@ export type InitializeUrlStateParams = { organization: Organization; defaultSelection?: Partial; 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. */ @@ -157,6 +161,7 @@ export function initializeUrlState({ skipLoadLastUsed, skipLoadLastUsedEnvironment, maxPickableDays, + maxDateRange, shouldPersist = true, shouldForceProject, defaultSelection, @@ -308,6 +313,7 @@ export function initializeUrlState({ } let shouldUseMaxPickableDays = false; + let shouldUseMaxDateRange = false; if (maxPickableDays && pageFilters.datetime) { let {start, end} = pageFilters.datetime; @@ -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, + }; + } } } } @@ -343,21 +366,31 @@ export function initializeUrlState({ ); } - const newDatetime = 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, - }; + let newDatetime: PageFiltersUpdate; + if (shouldUseMaxDateRange) { + newDatetime = { + period: `${maxDateRange}d`, + start: null, + end: null, + utc: datetime.utc, + }; + } else if (shouldUseMaxPickableDays) { + newDatetime = { + period: `${maxPickableDays}d`, + start: null, + end: null, + utc: datetime.utc, + }; + } else { + newDatetime = { + ...datetime, + period: + parsed.start || parsed.end || parsed.period || shouldUsePinnedDatetime + ? datetime.period + : null, + utc: parsed.utc || shouldUsePinnedDatetime ? datetime.utc : null, + }; + } if (!skipInitializeUrlParams) { updateParams({project, environment, ...newDatetime}, location, navigate, { diff --git a/static/app/components/pageFilters/container.spec.tsx b/static/app/components/pageFilters/container.spec.tsx index 2f0eaee741d6..9a7a316bdbc6 100644 --- a/static/app/components/pageFilters/container.spec.tsx +++ b/static/app/components/pageFilters/container.spec.tsx @@ -11,6 +11,7 @@ import {PageFiltersStore} from 'sentry/components/pageFilters/store'; import {OrganizationsStore} from 'sentry/stores/organizationsStore'; import {OrganizationStore} from 'sentry/stores/organizationStore'; import {ProjectsStore} from 'sentry/stores/projectsStore'; +import {getUtcToLocalDateObject} from 'sentry/utils/dates'; import {localStorageWrapper} from 'sentry/utils/localStorage'; describe('PageFiltersContainer', () => { @@ -561,6 +562,129 @@ describe('PageFiltersContainer', () => { }); }); + describe('maxDateRange param', () => { + it('resets period when maxDateRange appears and current selection exceeds it', async () => { + const {rerender} = render(, { + 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(); + + 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(, { + 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(); + + await waitFor(() => + expect(PageFiltersStore.getState().selection.datetime).toEqual({ + period: '7d', + utc: null, + start: null, + end: null, + }) + ); + }); + + it('does not reset period when selection is within maxPickableDays and maxDateRange', async () => { + const start = moment().subtract(14, 'days').format('YYYY-MM-DDTHH:mm:ss'); + const end = moment().subtract(8, 'days').format('YYYY-MM-DDTHH:mm:ss'); + render(, { + organization, + initialRouterConfig: { + location: { + pathname: '/organizations/org-slug/test/', + query: {start, end}, + }, + route: '/organizations/:orgId/test/', + }, + }); + + await waitFor(() => + expect(PageFiltersStore.getState().selection.datetime).toEqual({ + period: null, + utc: null, + start: getUtcToLocalDateObject(start), + end: getUtcToLocalDateObject(end), + }) + ); + }); + + 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(, { + 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(); + + await waitFor(() => + expect(PageFiltersStore.getState().selection.datetime).toEqual({ + period: '7d', + utc: null, + start: null, + end: null, + }) + ); + }); + }); + describe('skipInitializeUrlParams', () => { const skipInitProjects = [ ProjectFixture({id: '1', slug: 'staging-project', environments: ['staging']}), diff --git a/static/app/components/pageFilters/container.tsx b/static/app/components/pageFilters/container.tsx index 2b1d73b87cc3..5b43b009ec8a 100644 --- a/static/app/components/pageFilters/container.tsx +++ b/static/app/components/pageFilters/container.tsx @@ -16,6 +16,7 @@ import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {parseStatsPeriod} from 'sentry/components/timeRangeSelector/utils'; import {DEFAULT_STATS_PERIOD} from 'sentry/constants'; import {statsPeriodToDays} from 'sentry/utils/duration/statsPeriodToDays'; +import {DAY as DAY_IN_MS} from 'sentry/utils/formatters'; import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser'; import {useLocation} from 'sentry/utils/useLocation'; import {useDefaultMaxPickableDays} from 'sentry/utils/useMaxPickableDays'; @@ -58,6 +59,7 @@ export function PageFiltersContainer({ skipLoadLastUsed, skipLoadLastUsedEnvironment, maxPickableDays, + maxDateRange, children, ...props }: Props) { @@ -103,6 +105,7 @@ export function PageFiltersContainer({ skipLoadLastUsed, skipLoadLastUsedEnvironment, maxPickableDays, + maxDateRange, memberProjects, nonMemberProjects, defaultSelection, @@ -132,9 +135,10 @@ 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 @@ -142,10 +146,13 @@ export function PageFiltersContainer({ 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; } @@ -154,19 +161,39 @@ export function PageFiltersContainer({ // For relative periods (e.g., "14d"), check if the period exceeds the new max if (period) { - return statsPeriodToDays(period) > maxPickableDays; + 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 * DAY_IN_MS; + 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) { @@ -175,7 +202,7 @@ 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, @@ -183,7 +210,7 @@ export function PageFiltersContainer({ 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]); diff --git a/static/app/components/timeRangeSelector/utils.tsx b/static/app/components/timeRangeSelector/utils.tsx index eeaf386bfa60..a6e1d5250806 100644 --- a/static/app/components/timeRangeSelector/utils.tsx +++ b/static/app/components/timeRangeSelector/utils.tsx @@ -277,7 +277,7 @@ export const _timeRangeAutoCompleteFilter = function { @@ -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) @@ -54,6 +58,7 @@ export function useDatePageFilterProps({ defaultPeriod, isOptionDisabled, maxPickableDays, + maxDateRange, menuFooter, relativeOptions: ({arbitraryOptions}) => ({ ...arbitraryOptions, @@ -61,5 +66,5 @@ export function useDatePageFilterProps({ ...disabledOptions, }), }; - }, [defaultPeriod, maxPickableDays, maxUpgradableDays, upsellFooter]); + }, [defaultPeriod, maxDateRange, maxPickableDays, maxUpgradableDays, upsellFooter]); } diff --git a/static/app/utils/useMaxPickableDays.tsx b/static/app/utils/useMaxPickableDays.tsx index c82bee8eb02f..6fbc051ae318 100644 --- a/static/app/utils/useMaxPickableDays.tsx +++ b/static/app/utils/useMaxPickableDays.tsx @@ -35,6 +35,10 @@ export interface MaxPickableDaysOptions { */ maxUpgradableDays: NonNullable; defaultPeriod?: DatePageFilterProps['defaultPeriod']; + /** + * The maximum number of sequential days that can be selected on the date page filter + */ + maxDateRange?: number; upsellFooter?: ReactNode; } diff --git a/static/app/views/explore/spans/content.spec.tsx b/static/app/views/explore/spans/content.spec.tsx index 4c56614d70fc..f294d998e14f 100644 --- a/static/app/views/explore/spans/content.spec.tsx +++ b/static/app/views/explore/spans/content.spec.tsx @@ -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: [], diff --git a/static/app/views/explore/spans/content.tsx b/static/app/views/explore/spans/content.tsx index 5ecde70d0ac1..721c173ca54d 100644 --- a/static/app/views/explore/spans/content.tsx +++ b/static/app/views/explore/spans/content.tsx @@ -48,12 +48,6 @@ import {TraceItemDataset} from 'sentry/views/explore/types'; import {useOnboardingProject} from 'sentry/views/insights/common/queries/useOnboardingProject'; import {TopBar} from 'sentry/views/navigation/topBar'; -const CROSS_EVENTS_DATE_OVERRIDE: MaxPickableDaysOptions = { - defaultPeriod: MAX_PERIOD_FOR_CROSS_EVENTS, - maxPickableDays: MAX_DAYS_FOR_CROSS_EVENTS, - maxUpgradableDays: MAX_DAYS_FOR_CROSS_EVENTS, -}; - function useHasCrossEvents() { const crossEvents = useQueryParamsCrossEvents(); return defined(crossEvents) && crossEvents.length > 0; @@ -77,6 +71,13 @@ function ExploreContentInner() { dataCategories: [DataCategory.SPANS], }); + const CROSS_EVENTS_DATE_OVERRIDE: MaxPickableDaysOptions = { + defaultPeriod: MAX_PERIOD_FOR_CROSS_EVENTS, + maxPickableDays: dataCategoryMaxPickableDays.maxPickableDays, + maxUpgradableDays: MAX_DAYS_FOR_CROSS_EVENTS, + maxDateRange: MAX_DAYS_FOR_CROSS_EVENTS, + }; + const maxPickableDays = hasCrossEvents ? CROSS_EVENTS_DATE_OVERRIDE : dataCategoryMaxPickableDays; @@ -86,7 +87,10 @@ function ExploreContentInner() { return ( - + From 3ffc1a766563bc2ffd883593de1214497ce5d670 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki <44422760+DominikB2014@users.noreply.github.com> Date: Mon, 25 May 2026 11:23:13 -0400 Subject: [PATCH 5/5] fix(dashboards): propagate global filters in Open in Issues link (#116105) propogate global filters in open in issues link For example: 1. If i have a transaction filter on the issues dataset image 2. Clicking `open in issues` actually populates the query now image --- static/app/views/dashboards/utils.spec.tsx | 24 ++++++++++++++++++++++ static/app/views/dashboards/utils.tsx | 6 +++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/static/app/views/dashboards/utils.spec.tsx b/static/app/views/dashboards/utils.spec.tsx index ee76c15f8f82..8ed3833a318d 100644 --- a/static/app/views/dashboards/utils.spec.tsx +++ b/static/app/views/dashboards/utils.spec.tsx @@ -194,6 +194,30 @@ describe('Dashboards util', () => { const urlParams = new URLSearchParams(queryString); expect(urlParams.get('query')).toBe('(is:unresolved) release:["1.0.0","2.0.0"] '); }); + it('applies global filters scoped to the issue dataset', () => { + const url = getWidgetIssueUrl( + widget, + { + globalFilter: [ + { + dataset: WidgetType.ISSUE, + tag: {key: 'transaction', name: 'transaction'}, + value: 'transaction:/api/foo', + }, + { + dataset: WidgetType.DISCOVER, + tag: {key: 'transaction', name: 'transaction'}, + value: 'transaction:/api/bar', + }, + ], + }, + selection, + OrganizationFixture() + ); + const queryString = url.split('?')[1]; + const urlParams = new URLSearchParams(queryString); + expect(urlParams.get('query')).toBe('(is:unresolved) transaction:/api/foo'); + }); }); describe('flattenErrors', () => { diff --git a/static/app/views/dashboards/utils.tsx b/static/app/views/dashboards/utils.tsx index 32fb5a597ee8..49f3d37079cd 100644 --- a/static/app/views/dashboards/utils.tsx +++ b/static/app/views/dashboards/utils.tsx @@ -278,7 +278,11 @@ export function getWidgetIssueUrl( ? {start: getUtcDateString(start), end: getUtcDateString(end), utc} : {statsPeriod: period}; const issuesLocation = `/organizations/${organization.slug}/issues/?${qs.stringify({ - query: applyDashboardFilters(widget.queries?.[0]?.conditions, dashboardFilters), + query: applyDashboardFilters( + widget.queries?.[0]?.conditions, + dashboardFilters, + widget.widgetType + ), sort: widget.queries?.[0]?.orderby, ...datetime, // Pass empty string when projects is empty to preserve "My Projects" selection in URL