diff --git a/static/app/views/dashboards/types.tsx b/static/app/views/dashboards/types.tsx index d4f4ba45f8589b..2f0298193fbf46 100644 --- a/static/app/views/dashboards/types.tsx +++ b/static/app/views/dashboards/types.tsx @@ -94,11 +94,24 @@ export type WidgetQuery = { selectedAggregate?: number; }; +type WidgetChangedReason = { + equations: Array<{ + equation: string; + reason: string | string[]; + }> | null; + orderby: Array<{ + orderby: string; + reason: string; + }> | null; + selected_columns: string[]; +}; + export type Widget = { displayType: DisplayType; interval: string; queries: WidgetQuery[]; title: string; + changedReason?: WidgetChangedReason[]; dashboardId?: string; datasetSource?: DatasetSource; description?: string; diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx index d678fde93d631d..718ed5deff7078 100644 --- a/static/app/views/dashboards/widgetCard/index.tsx +++ b/static/app/views/dashboards/widgetCard/index.tsx @@ -47,6 +47,7 @@ import {WidgetViewerContext} from 'sentry/views/dashboards/widgetViewer/widgetVi import {useDashboardsMEPContext} from './dashboardsMEPContext'; import { getMenuOptions, + useDroppedColumnsWarning, useIndexedEventsWarning, useTransactionsDeprecationWarning, } from './widgetCardContextMenu'; @@ -195,6 +196,7 @@ function WidgetCard(props: Props) { widget, selection, }); + const droppedColumnsWarning = useDroppedColumnsWarning(widget); const sessionDurationWarning = hasSessionDuration ? SESSION_DURATION_ALERT_TEXT : null; const spanTimeRangeWarning = useTimeRangeWarning({widget}); @@ -266,6 +268,7 @@ function WidgetCard(props: Props) { sessionDurationWarning, spanTimeRangeWarning, transactionsDeprecationWarning, + droppedColumnsWarning, ].filter(Boolean) as string[]; const actionsDisabled = Boolean(props.isPreview); diff --git a/static/app/views/dashboards/widgetCard/widgetCardContextMenu.tsx b/static/app/views/dashboards/widgetCard/widgetCardContextMenu.tsx index 561b2e1b21a825..cba09e92566b0a 100644 --- a/static/app/views/dashboards/widgetCard/widgetCardContextMenu.tsx +++ b/static/app/views/dashboards/widgetCard/widgetCardContextMenu.tsx @@ -1,4 +1,5 @@ import {useMemo} from 'react'; +import styled from '@emotion/styled'; import type {Location} from 'history'; import qs from 'query-string'; @@ -8,6 +9,7 @@ import { } from 'sentry/actionCreators/modal'; import {openConfirmModal} from 'sentry/components/confirm'; import {Link} from 'sentry/components/core/link'; +import {Text} from 'sentry/components/core/text'; import type {MenuItemProps} from 'sentry/components/dropdownMenu'; import {t, tct} from 'sentry/locale'; import type {PageFilters} from 'sentry/types/core'; @@ -106,6 +108,70 @@ const createExploreUrl = ( return getExploreUrl({organization, selection, ...queryParams}); }; +export const useDroppedColumnsWarning = (widget: Widget): React.JSX.Element | null => { + if (!widget.changedReason) { + return null; + } + + const baseWarning = t( + "This widget may look different from its original query. Here's why:" + ); + const columnsWarning = []; + const equationsWarning = []; + const orderbyWarning = []; + for (const changedReason of widget.changedReason) { + if (changedReason.selected_columns.length > 0) { + columnsWarning.push( + tct(`The following fields were dropped: [columns].`, { + columns: changedReason.selected_columns.join(', '), + }) + ); + } + if (changedReason.equations) { + equationsWarning.push( + ...changedReason.equations.map(equation => + tct(`[equation] was dropped because [reason] is unsupported.`, { + equation: equation.equation, + reason: + typeof equation.reason === 'string' + ? equation.reason + : equation.reason.join(', '), + }) + ) + ); + } + if (changedReason.orderby) { + orderbyWarning.push( + ...changedReason.orderby.map(equation => + tct(`[orderby] was dropped because [reason].`, { + orderby: equation.orderby, + reason: equation.reason, + }) + ) + ); + } + } + + const allWarnings = [...columnsWarning, ...equationsWarning, ...orderbyWarning]; + + if (allWarnings.length > 0) { + return ( +
+ {baseWarning} + {allWarnings.map((warning, index) => ( + {warning} + ))} +
+ ); + } + + return null; +}; + +const StyledText = styled(Text)` + padding-bottom: ${p => p.theme.space.xs}; +`; + export function getMenuOptions( dashboardFilters: DashboardFilters | undefined, organization: Organization, diff --git a/static/app/views/explore/hooks/useGetSavedQueries.tsx b/static/app/views/explore/hooks/useGetSavedQueries.tsx index ba4a2696bf90e8..c6c54b6acefe19 100644 --- a/static/app/views/explore/hooks/useGetSavedQueries.tsx +++ b/static/app/views/explore/hooks/useGetSavedQueries.tsx @@ -6,6 +6,7 @@ import {defined} from 'sentry/utils'; import {useApiQuery, useQueryClient} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; import type {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; +import type {ExploreQueryChangedReason} from 'sentry/views/explore/hooks/useSaveQuery'; import {TraceItemDataset} from 'sentry/views/explore/types'; export type RawGroupBy = { @@ -100,6 +101,7 @@ type ReadableSavedQuery = { projects: number[]; query: [ReadableQuery, ...ReadableQuery[]]; starred: boolean; + changedReason?: ExploreQueryChangedReason | null; createdBy?: User; end?: string; environment?: string[]; @@ -120,6 +122,7 @@ export class SavedQuery { query: [SavedQueryQuery, ...SavedQueryQuery[]]; dataset: ReadableSavedQuery['dataset']; starred: boolean; + changedReason?: ExploreQueryChangedReason | null; createdBy?: User; end?: string | DateString; environment?: string[]; @@ -128,6 +131,7 @@ export class SavedQuery { start?: string | DateString; constructor(savedQuery: ReadableSavedQuery) { + this.changedReason = savedQuery.changedReason; this.dateAdded = savedQuery.dateAdded; this.dateUpdated = savedQuery.dateUpdated; this.id = savedQuery.id; diff --git a/static/app/views/explore/hooks/useSaveQuery.tsx b/static/app/views/explore/hooks/useSaveQuery.tsx index 7f49987bd96456..4eda739f9a6ca4 100644 --- a/static/app/views/explore/hooks/useSaveQuery.tsx +++ b/static/app/views/explore/hooks/useSaveQuery.tsx @@ -22,11 +22,24 @@ import {isGroupBy} from 'sentry/views/explore/queryParams/groupBy'; import type {ReadableQueryParams} from 'sentry/views/explore/queryParams/readableQueryParams'; import {isVisualize} from 'sentry/views/explore/queryParams/visualize'; +export type ExploreQueryChangedReason = { + columns: string[]; + equations: Array<{ + equation: string; + reason: string | string[]; + }> | null; + orderby: Array<{ + orderby: string; + reason: string; + }> | null; +}; + // Request payload type that matches the backend ExploreSavedQuerySerializer type ExploreSavedQueryRequest = { dataset: 'logs' | 'spans' | 'segment_spans'; name: string; projects: number[]; + changedReason?: ExploreQueryChangedReason; end?: DateString; environment?: string[]; interval?: string; diff --git a/static/app/views/explore/spans/droppedFieldsAlert.tsx b/static/app/views/explore/spans/droppedFieldsAlert.tsx new file mode 100644 index 00000000000000..b3fb4b9863a89b --- /dev/null +++ b/static/app/views/explore/spans/droppedFieldsAlert.tsx @@ -0,0 +1,86 @@ +import styled from '@emotion/styled'; + +import {Alert} from 'sentry/components/core/alert'; +import {Text} from 'sentry/components/core/text'; +import List from 'sentry/components/list'; +import ListItem from 'sentry/components/list/listItem'; +import {t, tct} from 'sentry/locale'; +import {useExploreId} from 'sentry/views/explore/contexts/pageParamsContext'; +import {useGetSavedQuery} from 'sentry/views/explore/hooks/useGetSavedQueries'; + +export function DroppedFieldsAlert(): React.JSX.Element | null { + const id = useExploreId(); + const {data: savedQuery} = useGetSavedQuery(id); + + if (!savedQuery) { + return null; + } + + if (!savedQuery.changedReason) { + return null; + } + + const baseWarning = t( + "This query may look different from the original query. Here's why:" + ); + const columnsWarning = []; + const equationsWarning = []; + const orderbyWarning = []; + + const changedReason = savedQuery.changedReason; + if (changedReason.columns.length > 0) { + columnsWarning.push( + tct(`The following fields were dropped: [columns]`, { + columns: changedReason.columns.join(', '), + }) + ); + } + if (changedReason.equations) { + equationsWarning.push( + ...changedReason.equations.map(equation => + tct(`[equation] was dropped because [reason] is unsupported`, { + equation: equation.equation, + reason: + typeof equation.reason === 'string' + ? equation.reason + : equation.reason.join(', '), + }) + ) + ); + } + if (changedReason.orderby) { + orderbyWarning.push( + ...changedReason.orderby.map(equation => + tct(`[orderby] was dropped because [reason]`, { + orderby: equation.orderby, + reason: equation.reason, + }) + ) + ); + } + + const allWarnings = [...columnsWarning, ...equationsWarning, ...orderbyWarning]; + + if (allWarnings.length > 0) { + return ( + + {baseWarning} + + {allWarnings.map((warning, index) => ( + {warning} + ))} + + + ); + } + + return null; +} + +const StyledText = styled(Text)` + padding-bottom: ${p => p.theme.space.xs}; +`; + +const StyledAlert = styled(Alert)` + margin-bottom: ${p => p.theme.space.md}; +`; diff --git a/static/app/views/explore/spans/spansTab.tsx b/static/app/views/explore/spans/spansTab.tsx index b9acdbbf2dd425..5c6466a9049b2f 100644 --- a/static/app/views/explore/spans/spansTab.tsx +++ b/static/app/views/explore/spans/spansTab.tsx @@ -67,6 +67,7 @@ import { useSetQueryParamsVisualizes, } from 'sentry/views/explore/queryParams/context'; import {ExploreCharts} from 'sentry/views/explore/spans/charts'; +import {DroppedFieldsAlert} from 'sentry/views/explore/spans/droppedFieldsAlert'; import {ExtrapolationEnabledAlert} from 'sentry/views/explore/spans/extrapolationEnabledAlert'; import {SettingsDropdown} from 'sentry/views/explore/spans/settingsDropdown'; import {SpansExport} from 'sentry/views/explore/spans/spansExport'; @@ -369,6 +370,7 @@ function SpanTabContentSection({ const visualizes = useQueryParamsVisualizes(); const setVisualizes = useSetQueryParamsVisualizes(); const extrapolate = useQueryParamsExtrapolate(); + const id = useExploreId(); const [tab, setTab] = useTab(); const [caseInsensitive] = useCaseInsensitivity(); @@ -487,6 +489,7 @@ function SpanTabContentSection({ + {defined(id) && } {!resultsLoading && !hasResults && ( )}