From 1004110185140ef3a9518af35c9ffb525a3ae131 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Thu, 20 Nov 2025 12:40:23 -0500 Subject: [PATCH 1/4] auto populate dashboard Ids in links --- static/app/utils/discover/fieldRenderers.tsx | 5 ++- static/app/views/dashboards/filtersBar.tsx | 15 +++++++- .../dashboards/prebuiltDashboardRenderer.tsx | 38 +++++++++++++++++-- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index 08afd7632d0162..327466d53b5111 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -1425,8 +1425,9 @@ function getDashboardUrl( linkedDashboard => linkedDashboard.field === field ); if (dashboardLink && dashboardLink.dashboardId !== '-1') { - const newTemporaryFilters: GlobalFilter[] = - dashboardFilters[DashboardFilterKeys.GLOBAL_FILTER] ?? []; + const newTemporaryFilters: GlobalFilter[] = [ + ...(dashboardFilters[DashboardFilterKeys.GLOBAL_FILTER] ?? []), + ].filter(filter => Boolean(filter.value)); // Format the value as a proper filter condition string const mutableSearch = new MutableSearch(''); diff --git a/static/app/views/dashboards/filtersBar.tsx b/static/app/views/dashboards/filtersBar.tsx index ee3ababa6a688c..73b04a73fbe7a9 100644 --- a/static/app/views/dashboards/filtersBar.tsx +++ b/static/app/views/dashboards/filtersBar.tsx @@ -90,10 +90,21 @@ export default function FiltersBar({ []; if (hasDrillDownFlowsFeature) { - return [ - ...globalFilters, + const finalFilters = [...globalFilters]; + const temporaryFilters = [ ...(dashboardFiltersFromURL?.[DashboardFilterKeys.TEMPORARY_FILTERS] ?? []), ]; + finalFilters.forEach(filter => { + // 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 = temporaryFilters.find( + tf => tf.dataset === filter.dataset && tf.tag.key === filter.tag.key + ); + if (temporaryFilter) { + filter.value = temporaryFilter.value; + temporaryFilters.splice(temporaryFilters.indexOf(temporaryFilter), 1); + } + }); + return [...finalFilters, ...temporaryFilters]; } return globalFilters; diff --git a/static/app/views/dashboards/prebuiltDashboardRenderer.tsx b/static/app/views/dashboards/prebuiltDashboardRenderer.tsx index 5e9db2a4215bdd..5238635211777b 100644 --- a/static/app/views/dashboards/prebuiltDashboardRenderer.tsx +++ b/static/app/views/dashboards/prebuiltDashboardRenderer.tsx @@ -1,6 +1,8 @@ import LoadingContainer from 'sentry/components/loading/loadingContainer'; import {defined} from 'sentry/utils'; +import {useApiQuery} from 'sentry/utils/queryClient'; import {useLocation} from 'sentry/utils/useLocation'; +import useOrganization from 'sentry/utils/useOrganization'; import useRouter from 'sentry/utils/useRouter'; import DashboardDetail from 'sentry/views/dashboards/detail'; import {DashboardState, type DashboardDetails} from 'sentry/views/dashboards/types'; @@ -59,6 +61,8 @@ export function PrebuiltDashboardRenderer({prebuiltId}: PrebuiltDashboardRendere const usePopulateLinkedDashboards = (dashboard: PrebuiltDashboard) => { const {widgets} = dashboard; + const organization = useOrganization(); + const linkedDashboardsWithStaticDashboardIds = widgets .flatMap(widget => { return widget.queries @@ -67,11 +71,39 @@ const usePopulateLinkedDashboards = (dashboard: PrebuiltDashboard) => { }) .filter(linkedDashboard => linkedDashboard.staticDashboardId !== undefined); - if (!linkedDashboardsWithStaticDashboardIds.length) { + const hasLinkedDashboards = linkedDashboardsWithStaticDashboardIds.length > 0; + const path = `/organizations/${organization.slug}/dashboards/`; + + const {data, isLoading} = useApiQuery( + [ + path, + { + query: { + prebuiltId: linkedDashboardsWithStaticDashboardIds.map( + d => d.staticDashboardId + ), + }, + }, + ], + { + enabled: hasLinkedDashboards, + staleTime: 0, + retry: false, + } + ); + + if (!hasLinkedDashboards) { return {dashboard, isLoading: false}; } - // TODO we should fetch the real dashboard id here, this requires BROWSE-128 + linkedDashboardsWithStaticDashboardIds.forEach(linkedDashboard => { + const dasboardId = data?.find( + d => d.prebuiltId === linkedDashboard.staticDashboardId + )?.id; + if (dasboardId) { + linkedDashboard.dashboardId = dasboardId; + } + }); - return {dashboard, isLoading: false}; + return {dashboard, isLoading}; }; From add6042d52655721ce4d0b39cb5cc8d330cacd11 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Thu, 20 Nov 2025 13:54:24 -0500 Subject: [PATCH 2/4] memoize stuff --- static/app/views/dashboards/filtersBar.tsx | 2 +- .../dashboards/prebuiltDashboardRenderer.tsx | 65 ++++++++++++------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/static/app/views/dashboards/filtersBar.tsx b/static/app/views/dashboards/filtersBar.tsx index 73b04a73fbe7a9..91e9c8512b28c6 100644 --- a/static/app/views/dashboards/filtersBar.tsx +++ b/static/app/views/dashboards/filtersBar.tsx @@ -100,7 +100,7 @@ export default function FiltersBar({ tf => tf.dataset === filter.dataset && tf.tag.key === filter.tag.key ); if (temporaryFilter) { - filter.value = temporaryFilter.value; + filter = {...filter, value: temporaryFilter.value}; temporaryFilters.splice(temporaryFilters.indexOf(temporaryFilter), 1); } }); diff --git a/static/app/views/dashboards/prebuiltDashboardRenderer.tsx b/static/app/views/dashboards/prebuiltDashboardRenderer.tsx index 5238635211777b..ba2153e02d805c 100644 --- a/static/app/views/dashboards/prebuiltDashboardRenderer.tsx +++ b/static/app/views/dashboards/prebuiltDashboardRenderer.tsx @@ -1,3 +1,5 @@ +import {useMemo} from 'react'; + import LoadingContainer from 'sentry/components/loading/loadingContainer'; import {defined} from 'sentry/utils'; import {useApiQuery} from 'sentry/utils/queryClient'; @@ -63,26 +65,27 @@ const usePopulateLinkedDashboards = (dashboard: PrebuiltDashboard) => { const {widgets} = dashboard; const organization = useOrganization(); - const linkedDashboardsWithStaticDashboardIds = widgets - .flatMap(widget => { - return widget.queries - .flatMap(query => query.linkedDashboards ?? []) - .filter(defined); - }) - .filter(linkedDashboard => linkedDashboard.staticDashboardId !== undefined); + const prebuiltIds = useMemo( + () => + widgets + .flatMap(widget => { + return widget.queries + .flatMap(query => query.linkedDashboards ?? []) + .filter(defined); + }) + .map(d => d.staticDashboardId) + .filter(defined), + [widgets] + ); - const hasLinkedDashboards = linkedDashboardsWithStaticDashboardIds.length > 0; + const hasLinkedDashboards = prebuiltIds.length > 0; const path = `/organizations/${organization.slug}/dashboards/`; const {data, isLoading} = useApiQuery( [ path, { - query: { - prebuiltId: linkedDashboardsWithStaticDashboardIds.map( - d => d.staticDashboardId - ), - }, + query: {prebuiltId: prebuiltIds.sort()}, }, ], { @@ -92,18 +95,30 @@ const usePopulateLinkedDashboards = (dashboard: PrebuiltDashboard) => { } ); - if (!hasLinkedDashboards) { - return {dashboard, isLoading: false}; - } - - linkedDashboardsWithStaticDashboardIds.forEach(linkedDashboard => { - const dasboardId = data?.find( - d => d.prebuiltId === linkedDashboard.staticDashboardId - )?.id; - if (dasboardId) { - linkedDashboard.dashboardId = dasboardId; + return useMemo(() => { + if (!hasLinkedDashboards || !data) { + return {dashboard, isLoading: false}; } - }); - return {dashboard, isLoading}; + const populatedDashboard = { + ...dashboard, + widgets: widgets.map(widget => ({ + ...widget, + queries: widget.queries.map(query => ({ + ...query, + linkedDashboards: query.linkedDashboards?.map(linkedDashboard => { + if (!linkedDashboard.staticDashboardId) { + return linkedDashboard; + } + const dashboardId = data.find( + d => d.prebuiltId === linkedDashboard.staticDashboardId + )?.id; + return dashboardId ? {...linkedDashboard, dashboardId} : linkedDashboard; + }), + })), + })), + }; + + return {dashboard: populatedDashboard, isLoading}; + }, [dashboard, widgets, data, hasLinkedDashboards, isLoading]); }; From 0c37ab0c33ea5917ca0f7fb87ee683b77fa20544 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Fri, 21 Nov 2025 12:09:28 -0500 Subject: [PATCH 3/4] fix --- static/app/views/dashboards/filtersBar.tsx | 23 +++++------------- static/app/views/dashboards/utils.tsx | 28 +++++++++++++++++++--- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/static/app/views/dashboards/filtersBar.tsx b/static/app/views/dashboards/filtersBar.tsx index 91e9c8512b28c6..9dc60329e87cba 100644 --- a/static/app/views/dashboards/filtersBar.tsx +++ b/static/app/views/dashboards/filtersBar.tsx @@ -24,7 +24,10 @@ import {globalFilterKeysAreEqual} from 'sentry/views/dashboards/globalFilter/uti import {useDatasetSearchBarData} from 'sentry/views/dashboards/hooks/useDatasetSearchBarData'; import {useHasDrillDownFlows} from 'sentry/views/dashboards/hooks/useHasDrillDownFlows'; import {useInvalidateStarredDashboards} from 'sentry/views/dashboards/hooks/useInvalidateStarredDashboards'; -import {getDashboardFiltersFromURL} from 'sentry/views/dashboards/utils'; +import { + getCombinedDashboardFilters, + getDashboardFiltersFromURL, +} from 'sentry/views/dashboards/utils'; import {checkUserHasEditAccess} from './utils/checkUserHasEditAccess'; import ReleasesSelectControl from './releasesSelectControl'; @@ -89,22 +92,8 @@ export default function FiltersBar({ filters?.[DashboardFilterKeys.GLOBAL_FILTER] ?? []; - if (hasDrillDownFlowsFeature) { - const finalFilters = [...globalFilters]; - const temporaryFilters = [ - ...(dashboardFiltersFromURL?.[DashboardFilterKeys.TEMPORARY_FILTERS] ?? []), - ]; - finalFilters.forEach(filter => { - // 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 = temporaryFilters.find( - tf => tf.dataset === filter.dataset && tf.tag.key === filter.tag.key - ); - if (temporaryFilter) { - filter = {...filter, value: temporaryFilter.value}; - temporaryFilters.splice(temporaryFilters.indexOf(temporaryFilter), 1); - } - }); - return [...finalFilters, ...temporaryFilters]; + if (hasDrillDownFlowsFeature && dashboardFiltersFromURL) { + return getCombinedDashboardFilters(dashboardFiltersFromURL); } return globalFilters; diff --git a/static/app/views/dashboards/utils.tsx b/static/app/views/dashboards/utils.tsx index e672a9b4c10fb9..dc4d8eae7f97b0 100644 --- a/static/app/views/dashboards/utils.tsx +++ b/static/app/views/dashboards/utils.tsx @@ -46,6 +46,7 @@ import {decodeList, decodeScalar} from 'sentry/utils/queryString'; import type { DashboardDetails, DashboardFilters, + GlobalFilter, Widget, WidgetQuery, } from 'sentry/views/dashboards/types'; @@ -607,11 +608,11 @@ export function dashboardFiltersToString( } } - const globalFilters = dashboardFilters?.[DashboardFilterKeys.GLOBAL_FILTER]; + const combinedFilters = getCombinedDashboardFilters(dashboardFilters); // If widgetType is provided, concatenate global filters that apply - if (widgetType && globalFilters) { + if (widgetType && combinedFilters) { dashboardFilterConditions += - globalFilters + combinedFilters .filter(globalFilter => globalFilter.dataset === widgetType) .map(globalFilter => globalFilter.value) .join(' ') ?? ''; @@ -620,6 +621,27 @@ export function dashboardFiltersToString( return dashboardFilterConditions; } +// Combines global and temporary filters into a single array, deduplicating by dataset and key prioritizing the temporary filter. +export function getCombinedDashboardFilters( + dashboardFilters: DashboardFilters | null | undefined +): GlobalFilter[] { + const finalFilters = [...(dashboardFilters?.[DashboardFilterKeys.GLOBAL_FILTER] ?? [])]; + const temporaryFilters = [ + ...(dashboardFilters?.[DashboardFilterKeys.TEMPORARY_FILTERS] ?? []), + ]; + 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 = temporaryFilters.find( + tf => tf.dataset === filter.dataset && tf.tag.key === filter.tag.key + ); + if (temporaryFilter) { + finalFilters[idx] = {...filter, value: temporaryFilter.value}; + temporaryFilters.splice(temporaryFilters.indexOf(temporaryFilter), 1); + } + }); + return [...finalFilters, ...temporaryFilters]; +} + export function connectDashboardCharts(groupName: string) { connect?.(groupName); } From 29bbc9b6b9993d50d795890e485662939ee004c4 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Fri, 21 Nov 2025 15:10:20 -0500 Subject: [PATCH 4/4] review --- static/app/views/dashboards/filtersBar.tsx | 5 ++++- static/app/views/dashboards/utils.tsx | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/static/app/views/dashboards/filtersBar.tsx b/static/app/views/dashboards/filtersBar.tsx index 9dc60329e87cba..620e1b33d1df99 100644 --- a/static/app/views/dashboards/filtersBar.tsx +++ b/static/app/views/dashboards/filtersBar.tsx @@ -93,7 +93,10 @@ export default function FiltersBar({ []; if (hasDrillDownFlowsFeature && dashboardFiltersFromURL) { - return getCombinedDashboardFilters(dashboardFiltersFromURL); + return getCombinedDashboardFilters( + globalFilters, + dashboardFiltersFromURL?.[DashboardFilterKeys.TEMPORARY_FILTERS] + ); } return globalFilters; diff --git a/static/app/views/dashboards/utils.tsx b/static/app/views/dashboards/utils.tsx index dc4d8eae7f97b0..7b67ed1fe406fa 100644 --- a/static/app/views/dashboards/utils.tsx +++ b/static/app/views/dashboards/utils.tsx @@ -608,7 +608,10 @@ export function dashboardFiltersToString( } } - const combinedFilters = getCombinedDashboardFilters(dashboardFilters); + const combinedFilters = getCombinedDashboardFilters( + dashboardFilters?.[DashboardFilterKeys.GLOBAL_FILTER], + dashboardFilters?.[DashboardFilterKeys.TEMPORARY_FILTERS] + ); // If widgetType is provided, concatenate global filters that apply if (widgetType && combinedFilters) { dashboardFilterConditions += @@ -623,23 +626,22 @@ export function dashboardFiltersToString( // Combines global and temporary filters into a single array, deduplicating by dataset and key prioritizing the temporary filter. export function getCombinedDashboardFilters( - dashboardFilters: DashboardFilters | null | undefined + globalFilters?: GlobalFilter[], + temporaryFilters?: GlobalFilter[] ): GlobalFilter[] { - const finalFilters = [...(dashboardFilters?.[DashboardFilterKeys.GLOBAL_FILTER] ?? [])]; - const temporaryFilters = [ - ...(dashboardFilters?.[DashboardFilterKeys.TEMPORARY_FILTERS] ?? []), - ]; + 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 = temporaryFilters.find( + const temporaryFilter = temporaryFiltersCopy.find( tf => tf.dataset === filter.dataset && tf.tag.key === filter.tag.key ); if (temporaryFilter) { finalFilters[idx] = {...filter, value: temporaryFilter.value}; - temporaryFilters.splice(temporaryFilters.indexOf(temporaryFilter), 1); + temporaryFiltersCopy.splice(temporaryFiltersCopy.indexOf(temporaryFilter), 1); } }); - return [...finalFilters, ...temporaryFilters]; + return [...finalFilters, ...temporaryFiltersCopy]; } export function connectDashboardCharts(groupName: string) {