Skip to content

Commit 45381d5

Browse files
authored
feat(dashboards): Apply global filters to widget queries (#101451)
- Appends global filters to widget queries of the same dataset - Warns users about conflicts between global filters and widget filters Fixes [DAIN-994](https://linear.app/getsentry/issue/DAIN-994/apply-global-filters-to-widget-queries)
1 parent 271ebe4 commit 45381d5

File tree

5 files changed

+80
-10
lines changed

5 files changed

+80
-10
lines changed

static/app/components/modals/widgetViewerModal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,10 @@ function WidgetViewerModal(props: Props) {
395395
const queryOptions = sortedQueries.map((query, index) => {
396396
const {name, conditions} = query;
397397
// Creates the highlighted query elements to be used in the Query Select
398-
const dashboardFiltersString = dashboardFiltersToString(dashboardFilters);
398+
const dashboardFiltersString = dashboardFiltersToString(
399+
dashboardFilters,
400+
widget.widgetType
401+
);
399402
const parsedQuery =
400403
!name && !!conditions
401404
? parseSearch(

static/app/views/dashboards/utils.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -576,12 +576,14 @@ export function getDashboardFiltersFromURL(location: Location): DashboardFilters
576576
}
577577

578578
export function dashboardFiltersToString(
579-
dashboardFilters: DashboardFilters | null | undefined
579+
dashboardFilters: DashboardFilters | null | undefined,
580+
widgetType?: WidgetType
580581
): string {
581582
let dashboardFilterConditions = '';
582-
const supportedFilters = omit(dashboardFilters, DashboardFilterKeys.GLOBAL_FILTER);
583-
if (supportedFilters) {
584-
for (const [key, activeFilters] of Object.entries(supportedFilters)) {
583+
584+
const pinnedFilters = omit(dashboardFilters, DashboardFilterKeys.GLOBAL_FILTER);
585+
if (pinnedFilters) {
586+
for (const [key, activeFilters] of Object.entries(pinnedFilters)) {
585587
if (activeFilters.length === 1) {
586588
dashboardFilterConditions += `${key}:"${activeFilters[0]}" `;
587589
} else if (activeFilters.length > 1) {
@@ -591,6 +593,17 @@ export function dashboardFiltersToString(
591593
}
592594
}
593595
}
596+
597+
const globalFilters = dashboardFilters?.[DashboardFilterKeys.GLOBAL_FILTER];
598+
// If widgetType is provided, concatenate global filters that apply
599+
if (widgetType && globalFilters) {
600+
dashboardFilterConditions +=
601+
globalFilters
602+
.filter(globalFilter => globalFilter.dataset === widgetType)
603+
.map(globalFilter => globalFilter.value)
604+
.join(' ') ?? '';
605+
}
606+
594607
return dashboardFilterConditions;
595608
}
596609

static/app/views/dashboards/widgetCard/genericWidgetQueries.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,10 @@ class GenericWidgetQueries<SeriesResponse, TableResponse> extends Component<
216216
applyDashboardFilters(widget: Widget): Widget {
217217
const {dashboardFilters, skipDashboardFilterParens} = this.props;
218218

219-
const dashboardFilterConditions = dashboardFiltersToString(dashboardFilters);
219+
const dashboardFilterConditions = dashboardFiltersToString(
220+
dashboardFilters,
221+
widget.widgetType
222+
);
220223
widget.queries.forEach(query => {
221224
if (dashboardFilterConditions) {
222225
// If there is no base query, there's no need to add parens

static/app/views/dashboards/widgetCard/index.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useContext, useEffect, useRef, useState} from 'react';
1+
import {useContext, useEffect, useMemo, useRef, useState} from 'react';
22
import styled from '@emotion/styled';
33
import type {LegendComponentOption} from 'echarts';
44
import type {Location} from 'history';
@@ -9,6 +9,8 @@ import ErrorBoundary from 'sentry/components/errorBoundary';
99
import {isWidgetViewerPath} from 'sentry/components/modals/widgetViewerModal/utils';
1010
import PanelAlert from 'sentry/components/panels/panelAlert';
1111
import Placeholder from 'sentry/components/placeholder';
12+
import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils';
13+
import {Token} from 'sentry/components/searchSyntax/parser';
1214
import {t, tct} from 'sentry/locale';
1315
import HookStore from 'sentry/stores/hookStore';
1416
import {space} from 'sentry/styles/space';
@@ -19,6 +21,7 @@ import type {Confidence, Organization} from 'sentry/types/organization';
1921
import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
2022
import type {AggregationOutputType, Sort} from 'sentry/utils/discover/fields';
2123
import {statsPeriodToDays} from 'sentry/utils/duration/statsPeriodToDays';
24+
import {getFieldDefinition} from 'sentry/utils/fields';
2225
import {hasOnDemandMetricWidgetFeature} from 'sentry/utils/onDemandMetrics/features';
2326
import {useExtractionStatus} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
2427
import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
@@ -34,6 +37,7 @@ import withSentryRouter from 'sentry/utils/withSentryRouter';
3437
import {DASHBOARD_CHART_GROUP} from 'sentry/views/dashboards/dashboard';
3538
import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types';
3639
import {
40+
DashboardFilterKeys,
3741
DisplayType,
3842
OnDemandExtractionState,
3943
WidgetType,
@@ -199,6 +203,10 @@ function WidgetCard(props: Props) {
199203
const droppedColumnsWarning = useDroppedColumnsWarning(widget);
200204
const sessionDurationWarning = hasSessionDuration ? SESSION_DURATION_ALERT_TEXT : null;
201205
const spanTimeRangeWarning = useTimeRangeWarning({widget});
206+
const conflictingFilterWarning = useConflictingFilterWarning({
207+
widget,
208+
dashboardFilters,
209+
});
202210

203211
const onDataFetchStart = () => {
204212
if (timeoutRef.current) {
@@ -269,6 +277,7 @@ function WidgetCard(props: Props) {
269277
spanTimeRangeWarning,
270278
transactionsDeprecationWarning,
271279
droppedColumnsWarning,
280+
conflictingFilterWarning,
272281
].filter(Boolean) as string[];
273282

274283
const actionsDisabled = Boolean(props.isPreview);
@@ -436,6 +445,45 @@ function useTimeRangeWarning({widget}: {widget: Widget}) {
436445
return null;
437446
}
438447

448+
// Displays a warning message if there is a conflict between widget and global filters
449+
function useConflictingFilterWarning({
450+
widget,
451+
dashboardFilters,
452+
}: {
453+
dashboardFilters: DashboardFilters | undefined;
454+
widget: Widget;
455+
}) {
456+
const conflictingFilterKeys = useMemo(() => {
457+
if (!dashboardFilters) return [];
458+
459+
const widgetFilterKeys = widget.queries.flatMap(query => {
460+
const parseResult = parseQueryBuilderValue(query.conditions, getFieldDefinition);
461+
if (!parseResult) {
462+
return [];
463+
}
464+
return parseResult
465+
.filter(token => token.type === Token.FILTER)
466+
.map(token => token.key.text);
467+
});
468+
const globalFilterKeys =
469+
dashboardFilters?.[DashboardFilterKeys.GLOBAL_FILTER]
470+
?.filter(filter => filter.dataset === widget.widgetType)
471+
.map(filter => filter.tag.key) ?? [];
472+
473+
const widgetFilterKeySet = new Set(widgetFilterKeys);
474+
return globalFilterKeys.filter(key => widgetFilterKeySet.has(key));
475+
}, [widget.queries, widget.widgetType, dashboardFilters]);
476+
477+
if (conflictingFilterKeys.length > 0) {
478+
return tct('[strong:Filter conflicts:] [filters]', {
479+
strong: <strong />,
480+
filters: conflictingFilterKeys.join(', '),
481+
});
482+
}
483+
484+
return null;
485+
}
486+
439487
const ErrorCard = styled(Placeholder)`
440488
display: flex;
441489
align-items: center;

static/app/views/dashboards/widgetCard/releaseWidgetQueries.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {useCallback, useEffect, useRef, useState} from 'react';
22
import cloneDeep from 'lodash/cloneDeep';
33
import isEqual from 'lodash/isEqual';
44
import omit from 'lodash/omit';
5-
import pick from 'lodash/pick';
65
import trimStart from 'lodash/trimStart';
76

87
import {addErrorMessage} from 'sentry/actionCreators/indicator';
@@ -22,7 +21,11 @@ import useOrganization from 'sentry/utils/useOrganization';
2221
import useProjects from 'sentry/utils/useProjects';
2322
import {ReleasesConfig} from 'sentry/views/dashboards/datasetConfig/releases';
2423
import type {DashboardFilters, Widget, WidgetQuery} from 'sentry/views/dashboards/types';
25-
import {DEFAULT_TABLE_LIMIT, DisplayType} from 'sentry/views/dashboards/types';
24+
import {
25+
DEFAULT_TABLE_LIMIT,
26+
DisplayType,
27+
WidgetType,
28+
} from 'sentry/views/dashboards/types';
2629
import {dashboardFiltersToString} from 'sentry/views/dashboards/utils';
2730
import {
2831
DERIVED_STATUS_METRICS_PATTERN,
@@ -246,7 +249,7 @@ function ReleaseWidgetQueries({
246249
environment: selection.environments,
247250
// Propagate release filters
248251
query: dashboardFilters
249-
? dashboardFiltersToString(pick(dashboardFilters, 'release'))
252+
? dashboardFiltersToString(dashboardFilters, WidgetType.RELEASE)
250253
: undefined,
251254
},
252255
}

0 commit comments

Comments
 (0)