Browse Source

feat(dashboards): Easy way to set split to transactions (#79639)

Add a way to set widget split to transaction for forced widgets. 
Also adds the warning to the widget viewer title.
![Screenshot 2024-10-24 at 4 33
34 PM](https://github.com/user-attachments/assets/2d0971b1-e8b7-47a8-87c4-33b76611b98d)
![Screenshot 2024-10-24 at 4 33
43 PM](https://github.com/user-attachments/assets/a4dfc54f-0871-4441-baa1-03d8f3cbfe1e)
Shruthi 4 months ago
parent
commit
f75b5f2ba4

+ 2 - 0
static/app/components/modals/widgetViewerModal.tsx

@@ -55,6 +55,7 @@ import {useLocation} from 'sentry/utils/useLocation';
 import {useNavigate} from 'sentry/utils/useNavigate';
 import useProjects from 'sentry/utils/useProjects';
 import withPageFilters from 'sentry/utils/withPageFilters';
+import {DiscoverSplitAlert} from 'sentry/views/dashboards/discoverSplitAlert';
 import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types';
 import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
 import {
@@ -1021,6 +1022,7 @@ function WidgetViewerModal(props: Props) {
                     <WidgetHeader>
                       <WidgetTitleRow>
                         <h3>{widget.title}</h3>
+                        <DiscoverSplitAlert widget={widget} />
                       </WidgetTitleRow>
                       {widget.description && (
                         <Tooltip

+ 47 - 0
static/app/views/dashboards/dashboard.spec.tsx

@@ -7,6 +7,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
 import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
 
 import MemberListStore from 'sentry/stores/memberListStore';
+import {DatasetSource} from 'sentry/utils/discover/types';
 import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
 import Dashboard from 'sentry/views/dashboards/dashboard';
 import type {Widget} from 'sentry/views/dashboards/types';
@@ -243,6 +244,52 @@ describe('Dashboards > Dashboard', () => {
     expect(mockCallbackToUnsetNewWidget).not.toHaveBeenCalled();
   });
 
+  it('updates the widget dataset split', async () => {
+    const splitWidget = {
+      ...newWidget,
+      widgetType: WidgetType.ERRORS,
+      datasetSource: DatasetSource.FORCED,
+    };
+    const splitWidgets = [splitWidget];
+    const dashboardWithOneWidget = {...mockDashboard, widgets: splitWidgets};
+
+    const mockOnUpdate = jest.fn();
+    const mockHandleUpdateWidgetList = jest.fn();
+
+    render(
+      <OrganizationContext.Provider value={initialData.organization}>
+        <MEPSettingProvider forceTransactions={false}>
+          <Dashboard
+            paramDashboardId="1"
+            dashboard={dashboardWithOneWidget}
+            organization={initialData.organization}
+            isEditingDashboard={false}
+            onUpdate={mockOnUpdate}
+            handleUpdateWidgetList={mockHandleUpdateWidgetList}
+            handleAddCustomWidget={() => undefined}
+            router={initialData.router}
+            location={initialData.router.location}
+            widgetLimitReached={false}
+            onSetNewWidget={() => undefined}
+            widgetLegendState={widgetLegendState}
+          />
+        </MEPSettingProvider>
+      </OrganizationContext.Provider>
+    );
+
+    await userEvent.hover(screen.getByLabelText('Dataset split warning'));
+
+    expect(
+      await screen.findByText(/We're splitting our datasets up/)
+    ).toBeInTheDocument();
+
+    await userEvent.click(await screen.findByText(/Switch to Transactions/));
+    await waitFor(() => {
+      expect(mockOnUpdate).toHaveBeenCalled();
+      expect(mockHandleUpdateWidgetList).toHaveBeenCalled();
+    });
+  });
+
   describe('Issue Widgets', () => {
     beforeEach(() => {
       MemberListStore.init();

+ 26 - 0
static/app/views/dashboards/dashboard.tsx

@@ -25,6 +25,7 @@ import type {PageFilters} from 'sentry/types/core';
 import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
 import type {Organization} from 'sentry/types/organization';
 import {trackAnalytics} from 'sentry/utils/analytics';
+import {DatasetSource} from 'sentry/utils/discover/types';
 import {hasCustomMetrics} from 'sentry/utils/metrics/features';
 import theme from 'sentry/utils/theme';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
@@ -91,6 +92,7 @@ type Props = {
   widgetLegendState: WidgetLegendSelectionState;
   widgetLimitReached: boolean;
   handleAddMetricWidget?: (layout?: Widget['layout']) => void;
+  handleChangeSplitDataset?: (widget: Widget, index: number) => void;
   isPreview?: boolean;
   newWidget?: Widget;
   onSetNewWidget?: () => void;
@@ -335,6 +337,29 @@ class Dashboard extends Component<Props, State> {
     }
   };
 
+  handleChangeSplitDataset = (widget: Widget, index: number) => {
+    const {dashboard, onUpdate, isEditingDashboard, handleUpdateWidgetList} = this.props;
+
+    const widgetCopy = cloneDeep({
+      ...widget,
+      id: undefined,
+    });
+
+    const nextList = [...dashboard.widgets];
+    const nextWidgetData = {
+      ...widgetCopy,
+      widgetType: WidgetType.TRANSACTIONS,
+      datasetSource: DatasetSource.USER,
+      id: widget.id,
+    };
+    nextList[index] = nextWidgetData;
+
+    onUpdate(nextList);
+    if (!isEditingDashboard) {
+      handleUpdateWidgetList(nextList);
+    }
+  };
+
   handleEditWidget = (index: number) => () => {
     const {organization, router, location, paramDashboardId} = this.props;
     const widget = this.props.dashboard.widgets[index];
@@ -396,6 +421,7 @@ class Dashboard extends Component<Props, State> {
       onDelete: this.handleDeleteWidget(widget),
       onEdit: this.handleEditWidget(index),
       onDuplicate: this.handleDuplicateWidget(widget, index),
+      onSetTransactionsDataset: () => this.handleChangeSplitDataset(widget, index),
 
       isPreview,
 

+ 6 - 1
static/app/views/dashboards/discoverSplitAlert.spec.tsx

@@ -5,6 +5,7 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
 import {DatasetSource} from 'sentry/utils/discover/types';
 import localStorage from 'sentry/utils/localStorage';
 import {DiscoverSplitAlert} from 'sentry/views/dashboards/discoverSplitAlert';
+import {WidgetType} from 'sentry/views/dashboards/types';
 
 describe('DiscoverSplitAlert', () => {
   beforeEach(() => {
@@ -14,7 +15,11 @@ describe('DiscoverSplitAlert', () => {
   it('renders if the widget has a forced split decision', async () => {
     render(
       <DiscoverSplitAlert
-        widget={{...WidgetFixture(), datasetSource: DatasetSource.FORCED}}
+        widget={{
+          ...WidgetFixture(),
+          datasetSource: DatasetSource.FORCED,
+          widgetType: WidgetType.ERRORS,
+        }}
       />
     );
 

+ 32 - 9
static/app/views/dashboards/discoverSplitAlert.tsx

@@ -1,25 +1,48 @@
 import {Tooltip} from 'sentry/components/tooltip';
 import {IconWarning} from 'sentry/icons';
-import {t} from 'sentry/locale';
+import {t, tct} from 'sentry/locale';
 import {DatasetSource} from 'sentry/utils/discover/types';
-import type {Widget} from 'sentry/views/dashboards/types';
+import {type Widget, WidgetType} from 'sentry/views/dashboards/types';
 
 interface DiscoverSplitAlertProps {
   widget: Widget;
+  onSetTransactionsDataset?: () => void;
 }
 
-export function useDiscoverSplitAlert({widget}: DiscoverSplitAlertProps): string | null {
-  if (widget?.datasetSource !== DatasetSource.FORCED) {
+export function useDiscoverSplitAlert({
+  widget,
+  onSetTransactionsDataset,
+}: DiscoverSplitAlertProps): JSX.Element | null {
+  if (
+    widget?.datasetSource !== DatasetSource.FORCED ||
+    widget?.widgetType !== WidgetType.ERRORS
+  ) {
     return null;
   }
 
-  return t(
-    "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Errors. Edit as you see fit."
+  return tct(
+    "We're splitting our datasets up to make it a bit easier to digest. We defaulted this widget to Errors. [editText]",
+    {
+      editText: onSetTransactionsDataset ? (
+        <a
+          onClick={() => {
+            onSetTransactionsDataset();
+          }}
+        >
+          {t('Switch to Transactions')}
+        </a>
+      ) : (
+        t('Edit as you see fit.')
+      ),
+    }
   );
 }
 
-export function DiscoverSplitAlert({widget}: DiscoverSplitAlertProps) {
-  const splitAlert = useDiscoverSplitAlert({widget});
+export function DiscoverSplitAlert({
+  widget,
+  onSetTransactionsDataset,
+}: DiscoverSplitAlertProps) {
+  const splitAlert = useDiscoverSplitAlert({widget, onSetTransactionsDataset});
 
   if (widget?.datasetSource !== DatasetSource.FORCED) {
     return null;
@@ -27,7 +50,7 @@ export function DiscoverSplitAlert({widget}: DiscoverSplitAlertProps) {
 
   if (splitAlert) {
     return (
-      <Tooltip containerDisplayMode="inline-flex" title={splitAlert}>
+      <Tooltip containerDisplayMode="inline-flex" isHoverable title={splitAlert}>
         <IconWarning color="warningText" aria-label={t('Dataset split warning')} />
       </Tooltip>
     );

+ 3 - 0
static/app/views/dashboards/sortableWidget.tsx

@@ -16,6 +16,7 @@ type Props = {
   onDelete: () => void;
   onDuplicate: () => void;
   onEdit: () => void;
+  onSetTransactionsDataset: () => void;
   widget: Widget;
   widgetLegendState: WidgetLegendSelectionState;
   widgetLimitReached: boolean;
@@ -33,6 +34,7 @@ function SortableWidget(props: Props) {
     onDelete,
     onEdit,
     onDuplicate,
+    onSetTransactionsDataset,
     isPreview,
     isMobile,
     windowWidth,
@@ -48,6 +50,7 @@ function SortableWidget(props: Props) {
     onDelete,
     onEdit,
     onDuplicate,
+    onSetTransactionsDataset,
     showContextMenu: true,
     isPreview,
     index,

+ 5 - 1
static/app/views/dashboards/widgetCard/index.spec.tsx

@@ -785,7 +785,11 @@ describe('Dashboards > WidgetCard', function () {
   });
 
   it('displays the discover split warning icon when the dataset source is forced', async function () {
-    const testWidget = {...WidgetFixture(), datasetSource: DatasetSource.FORCED};
+    const testWidget = {
+      ...WidgetFixture(),
+      datasetSource: DatasetSource.FORCED,
+      widgetType: WidgetType.ERRORS,
+    };
 
     renderWithProviders(
       <WidgetCard

+ 5 - 1
static/app/views/dashboards/widgetCard/index.tsx

@@ -77,6 +77,7 @@ type Props = WithRouterProps & {
   onDuplicate?: () => void;
   onEdit?: () => void;
   onLegendSelectChanged?: () => void;
+  onSetTransactionsDataset?: () => void;
   onUpdate?: (widget: Widget | null) => void;
   onWidgetSplitDecision?: (splitDecision: WidgetType) => void;
   renderErrorMessage?: (errorMessage?: string) => React.ReactNode;
@@ -186,7 +187,10 @@ function WidgetCard(props: Props) {
                 </Tooltip>
                 <ExtractedMetricsTag queryKey={widget} />
                 <DisplayOnDemandWarnings widget={widget} />
-                <DiscoverSplitAlert widget={widget} />
+                <DiscoverSplitAlert
+                  widget={widget}
+                  onSetTransactionsDataset={props.onSetTransactionsDataset}
+                />
               </WidgetTitleRow>
             </WidgetHeaderDescription>
             {!props.isEditingDashboard && (