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..620e1b33d1df99 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,11 +92,11 @@ export default function FiltersBar({ filters?.[DashboardFilterKeys.GLOBAL_FILTER] ?? []; - if (hasDrillDownFlowsFeature) { - return [ - ...globalFilters, - ...(dashboardFiltersFromURL?.[DashboardFilterKeys.TEMPORARY_FILTERS] ?? []), - ]; + if (hasDrillDownFlowsFeature && dashboardFiltersFromURL) { + return getCombinedDashboardFilters( + globalFilters, + dashboardFiltersFromURL?.[DashboardFilterKeys.TEMPORARY_FILTERS] + ); } return globalFilters; diff --git a/static/app/views/dashboards/prebuiltDashboardRenderer.tsx b/static/app/views/dashboards/prebuiltDashboardRenderer.tsx index 5e9db2a4215bdd..ba2153e02d805c 100644 --- a/static/app/views/dashboards/prebuiltDashboardRenderer.tsx +++ b/static/app/views/dashboards/prebuiltDashboardRenderer.tsx @@ -1,6 +1,10 @@ +import {useMemo} from 'react'; + 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,19 +63,62 @@ export function PrebuiltDashboardRenderer({prebuiltId}: PrebuiltDashboardRendere const usePopulateLinkedDashboards = (dashboard: PrebuiltDashboard) => { const {widgets} = dashboard; - const linkedDashboardsWithStaticDashboardIds = widgets - .flatMap(widget => { - return widget.queries - .flatMap(query => query.linkedDashboards ?? []) - .filter(defined); - }) - .filter(linkedDashboard => linkedDashboard.staticDashboardId !== undefined); + const organization = useOrganization(); + + const prebuiltIds = useMemo( + () => + widgets + .flatMap(widget => { + return widget.queries + .flatMap(query => query.linkedDashboards ?? []) + .filter(defined); + }) + .map(d => d.staticDashboardId) + .filter(defined), + [widgets] + ); + + const hasLinkedDashboards = prebuiltIds.length > 0; + const path = `/organizations/${organization.slug}/dashboards/`; + + const {data, isLoading} = useApiQuery( + [ + path, + { + query: {prebuiltId: prebuiltIds.sort()}, + }, + ], + { + enabled: hasLinkedDashboards, + staleTime: 0, + retry: false, + } + ); - if (!linkedDashboardsWithStaticDashboardIds.length) { - return {dashboard, isLoading: false}; - } + return useMemo(() => { + if (!hasLinkedDashboards || !data) { + return {dashboard, isLoading: false}; + } - // TODO we should fetch the real dashboard id here, this requires BROWSE-128 + 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, isLoading: false}; + return {dashboard: populatedDashboard, isLoading}; + }, [dashboard, widgets, data, hasLinkedDashboards, isLoading]); }; diff --git a/static/app/views/dashboards/utils.tsx b/static/app/views/dashboards/utils.tsx index e672a9b4c10fb9..7b67ed1fe406fa 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,14 @@ export function dashboardFiltersToString( } } - const globalFilters = dashboardFilters?.[DashboardFilterKeys.GLOBAL_FILTER]; + const combinedFilters = getCombinedDashboardFilters( + dashboardFilters?.[DashboardFilterKeys.GLOBAL_FILTER], + dashboardFilters?.[DashboardFilterKeys.TEMPORARY_FILTERS] + ); // 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 +624,26 @@ 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( + globalFilters?: GlobalFilter[], + temporaryFilters?: GlobalFilter[] +): 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}; + temporaryFiltersCopy.splice(temporaryFiltersCopy.indexOf(temporaryFilter), 1); + } + }); + return [...finalFilters, ...temporaryFiltersCopy]; +} + export function connectDashboardCharts(groupName: string) { connect?.(groupName); }