Skip to content
Open
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
10 changes: 9 additions & 1 deletion static/app/views/dashboards/detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,14 @@ class DashboardDetail extends Component<Props, State> {
)
: [''];

filterParams[DashboardFilterKeys.TEMPORARY_FILTERS] = activeFilters[
DashboardFilterKeys.TEMPORARY_FILTERS
]?.length
? activeFilters[DashboardFilterKeys.TEMPORARY_FILTERS].map(filter =>
JSON.stringify(filter)
)
: [''];

if (
!isEqualWith(activeFilters, dashboard.filters, (a, b) => {
// This is to handle the case where dashboard filters has release:[] and the new filter is release:""
Expand Down Expand Up @@ -1187,7 +1195,7 @@ class DashboardDetail extends Component<Props, State> {
isPreview={this.isPreview}
onDashboardFilterChange={this.handleChangeFilter}
shouldBusySaveButton={this.state.isSavingDashboardFilters}
isPrebuiltDashboard={defined(dashboard.prebuiltId)}
prebuiltDashboardId={dashboard.prebuiltId}
onCancel={() => {
resetPageFilters(dashboard, location);
trackAnalytics('dashboards2.filter.cancel', {
Expand Down
26 changes: 23 additions & 3 deletions static/app/views/dashboards/filtersBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilt
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {User} from 'sentry/types/user';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {ToggleOnDemand} from 'sentry/utils/performance/contexts/onDemandControl';
import {ReleasesProvider} from 'sentry/utils/releases/releasesProvider';
Expand All @@ -28,6 +29,10 @@ import {
getCombinedDashboardFilters,
getDashboardFiltersFromURL,
} from 'sentry/views/dashboards/utils';
import {
PREBUILT_DASHBOARDS,
type PrebuiltDashboardId,
} from 'sentry/views/dashboards/utils/prebuiltConfigs';

import {checkUserHasEditAccess} from './utils/checkUserHasEditAccess';
import ReleasesSelectControl from './releasesSelectControl';
Expand All @@ -44,9 +49,9 @@ type FiltersBarProps = {
onDashboardFilterChange: (activeFilters: DashboardFilters) => void;
dashboardCreator?: User;
dashboardPermissions?: DashboardPermissions;
isPrebuiltDashboard?: boolean;
onCancel?: () => void;
onSave?: () => Promise<void>;
prebuiltDashboardId?: PrebuiltDashboardId;
shouldBusySaveButton?: boolean;
};

Expand All @@ -63,14 +68,18 @@ export default function FiltersBar({
onDashboardFilterChange,
onSave,
shouldBusySaveButton,
isPrebuiltDashboard,
prebuiltDashboardId,
}: FiltersBarProps) {
const {selection} = usePageFilters();
const organization = useOrganization();
const currentUser = useUser();
const {teams: userTeams} = useUserTeams();
const getSearchBarData = useDatasetSearchBarData();
const hasDrillDownFlowsFeature = useHasDrillDownFlows();
const isPrebuiltDashboard = defined(prebuiltDashboardId);
const prebuiltDashboardFilters: GlobalFilter[] = prebuiltDashboardId
? (PREBUILT_DASHBOARDS[prebuiltDashboardId].filters.globalFilter ?? [])
: [];

const hasEditAccess = checkUserHasEditAccess(
currentUser,
Expand Down Expand Up @@ -106,9 +115,12 @@ export default function FiltersBar({

const updateGlobalFilters = (newGlobalFilters: GlobalFilter[]) => {
setActiveGlobalFilters(newGlobalFilters);
const temporaryFilters = newGlobalFilters.filter(filter => filter.isTemporary);
const globalFilters = newGlobalFilters.filter(filter => !filter.isTemporary);
onDashboardFilterChange({
[DashboardFilterKeys.RELEASE]: selectedReleases,
[DashboardFilterKeys.GLOBAL_FILTER]: newGlobalFilters,
[DashboardFilterKeys.GLOBAL_FILTER]: globalFilters,
[DashboardFilterKeys.TEMPORARY_FILTERS]: temporaryFilters,
});
};

Expand Down Expand Up @@ -163,6 +175,14 @@ export default function FiltersBar({
<Fragment>
{activeGlobalFilters.map(filter => (
<GenericFilterSelector
disableRemoveFilter={
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Release filter change clears temporary filters from URL

When the release filter is changed via ReleasesSelectControl, the handler calls onDashboardFilterChange without including TEMPORARY_FILTERS. The new code in detail.tsx always sets filterParams[TEMPORARY_FILTERS] - when it's not provided, it defaults to ['']. This overwrites any existing temporaryFilters in the URL, effectively clearing temporary filters when users change the release filter. Before this PR, the TEMPORARY_FILTERS key wasn't set in filterParams, so existing URL values were preserved. The fix requires the release handler to separate and pass temporary filters like updateGlobalFilters does.

Additional Locations (1)

Fix in Cursor Fix in Web

isPrebuiltDashboard &&
prebuiltDashboardFilters.some(
prebuiltFilter =>
prebuiltFilter.tag.key === filter.tag.key &&
prebuiltFilter.dataset === filter.dataset
)
}
key={filter.tag.key + filter.value}
globalFilter={filter}
searchBarData={getSearchBarData(filter.dataset)}
Expand Down
18 changes: 11 additions & 7 deletions static/app/views/dashboards/globalFilter/filterSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ type FilterSelectorProps = {
onRemoveFilter: (filter: GlobalFilter) => void;
onUpdateFilter: (filter: GlobalFilter) => void;
searchBarData: SearchBarData;
disableRemoveFilter?: boolean;
};

function FilterSelector({
globalFilter,
searchBarData,
onRemoveFilter,
onUpdateFilter,
disableRemoveFilter,
}: FilterSelectorProps) {
const {selection} = usePageFilters();

Expand Down Expand Up @@ -275,13 +277,15 @@ function FilterSelector({
{t('Clear')}
</StyledButton>
)}
<StyledButton
aria-label={t('Remove Filter')}
size="zero"
onClick={() => onRemoveFilter(globalFilter)}
>
{t('Remove Filter')}
</StyledButton>
{!disableRemoveFilter && (
<StyledButton
aria-label={t('Remove Filter')}
size="zero"
onClick={() => onRemoveFilter(globalFilter)}
>
{t('Remove Filter')}
</StyledButton>
)}
</Flex>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type GenericFilterSelectorProps = {
onRemoveFilter: (filter: GlobalFilter) => void;
onUpdateFilter: (filter: GlobalFilter) => void;
searchBarData: SearchBarData;
disableRemoveFilter?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: NumericFilterSelector ignores disableRemoveFilter prop

The NumericFilterSelector component doesn't destructure or use the disableRemoveFilter prop from GenericFilterSelectorProps, even though the PR adds this prop to disable removing filters from prebuilt dashboards. As a result, numeric filters on prebuilt dashboards will always show the "Remove Filter" button (rendered at line 294-301), defeating the intended protection for prebuilt dashboard filters. The prop is correctly added to FilterSelector but is missing from NumericFilterSelector.

Fix in Cursor Fix in Web

};

function getFilterSelector(
Expand Down
1 change: 1 addition & 0 deletions static/app/views/dashboards/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export type GlobalFilter = {
tag: Tag;
// The raw filter condition string (e.g. 'tagKey:[values,...]')
value: string;
isTemporary?: boolean;
Copy link
Member

Choose a reason for hiding this comment

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

With this new isTemporary field, would it be worth using it as the main separator between global/temp filters instead of using a new temporary filters parameter?

};

/**
Expand Down
6 changes: 4 additions & 2 deletions static/app/views/dashboards/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ export function getDashboardFiltersFromURL(location: Location): DashboardFilters
dashboardFilters[key] = queryFilters
.map(filter => {
try {
return JSON.parse(filter);
return {...JSON.parse(filter), isTemporary: true};
} catch (error) {
return null;
}
Expand Down Expand Up @@ -633,16 +633,18 @@ export function getCombinedDashboardFilters(
): GlobalFilter[] {
const finalFilters = [...(globalFilters ?? [])];
const temporaryFiltersCopy = [...(temporaryFilters ?? [])];

finalFilters.forEach((filter, idx) => {
// if a temporary filter exists for the same dataset and key, override it and delete it from the temporary filters to avoid duplicates
const temporaryFilter = temporaryFiltersCopy.find(
tf => tf.dataset === filter.dataset && tf.tag.key === filter.tag.key
);
if (temporaryFilter) {
finalFilters[idx] = {...filter, value: temporaryFilter.value};
finalFilters[idx] = {...temporaryFilter};
temporaryFiltersCopy.splice(temporaryFiltersCopy.indexOf(temporaryFilter), 1);
}
});

return [...finalFilters, ...temporaryFiltersCopy];
}

Expand Down
Loading