Browse Source

feat(dnd-transaction-summary-links): Added links. (#49093)

Feature request: https://github.com/getsentry/sentry/issues/43311

1. Added links to transaction fields in dashboard widget preview, widget
viewer modal and discover table. Mimics the functionality of 'Go to
summary' cell action in discover table columns.
2. Added tests.

---------

Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
Abdkhan14 1 year ago
parent
commit
d175499535

+ 25 - 2
static/app/components/charts/simpleTableChart.tsx

@@ -14,9 +14,14 @@ import {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
 import EventView, {MetaType} from 'sentry/utils/discover/eventView';
 import EventView, {MetaType} from 'sentry/utils/discover/eventView';
 import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
 import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
 import {fieldAlignment} from 'sentry/utils/discover/fields';
 import {fieldAlignment} from 'sentry/utils/discover/fields';
+import useProjects from 'sentry/utils/useProjects';
 import withOrganization from 'sentry/utils/withOrganization';
 import withOrganization from 'sentry/utils/withOrganization';
+import {StyledLink} from 'sentry/views/discover/table/tableView';
 import TopResultsIndicator from 'sentry/views/discover/table/topResultsIndicator';
 import TopResultsIndicator from 'sentry/views/discover/table/topResultsIndicator';
-import {decodeColumnOrder} from 'sentry/views/discover/utils';
+import {
+  decodeColumnOrder,
+  getTargetForTransactionSummaryLink,
+} from 'sentry/views/discover/utils';
 
 
 type Props = {
 type Props = {
   eventView: EventView;
   eventView: EventView;
@@ -57,6 +62,7 @@ function SimpleTableChart({
   fieldAliases,
   fieldAliases,
   loader,
   loader,
 }: Props) {
 }: Props) {
+  const {projects} = useProjects();
   function renderRow(
   function renderRow(
     index: number,
     index: number,
     row: TableDataRow,
     row: TableDataRow,
@@ -69,12 +75,29 @@ function SimpleTableChart({
         getFieldRenderer(column.key, tableMeta);
         getFieldRenderer(column.key, tableMeta);
 
 
       const unit = tableMeta.units?.[column.key];
       const unit = tableMeta.units?.[column.key];
+      let cell = fieldRenderer(row, {organization, location, eventView, unit});
+
+      if (column.key === 'transaction' && row.transaction) {
+        cell = (
+          <StyledLink
+            to={getTargetForTransactionSummaryLink(
+              row,
+              organization,
+              projects,
+              eventView
+            )}
+          >
+            {cell}
+          </StyledLink>
+        );
+      }
+
       return (
       return (
         <TableCell key={`${index}-${columnIndex}:${column.name}`}>
         <TableCell key={`${index}-${columnIndex}:${column.name}`}>
           {topResultsIndicators && columnIndex === 0 && (
           {topResultsIndicators && columnIndex === 0 && (
             <TopResultsIndicator count={topResultsIndicators} index={index} />
             <TopResultsIndicator count={topResultsIndicators} index={index} />
           )}
           )}
-          {fieldRenderer(row, {organization, location, eventView, unit})}
+          {cell}
         </TableCell>
         </TableCell>
       );
       );
     });
     });

+ 55 - 1
static/app/components/modals/widgetViewerModal.spec.tsx

@@ -7,6 +7,7 @@ import {ModalRenderProps} from 'sentry/actionCreators/modal';
 import WidgetViewerModal from 'sentry/components/modals/widgetViewerModal';
 import WidgetViewerModal from 'sentry/components/modals/widgetViewerModal';
 import MemberListStore from 'sentry/stores/memberListStore';
 import MemberListStore from 'sentry/stores/memberListStore';
 import PageFiltersStore from 'sentry/stores/pageFiltersStore';
 import PageFiltersStore from 'sentry/stores/pageFiltersStore';
+import ProjectsStore from 'sentry/stores/projectsStore';
 import {space} from 'sentry/styles/space';
 import {space} from 'sentry/styles/space';
 import {Series} from 'sentry/types/echarts';
 import {Series} from 'sentry/types/echarts';
 import {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
 import {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
@@ -91,7 +92,7 @@ describe('Modals -> WidgetViewerModal', function () {
         location: {query: {}},
         location: {query: {}},
       },
       },
       project: 1,
       project: 1,
-      projects: [],
+      projects: [TestStubs.Project()],
     });
     });
 
 
     initialDataWithFlag = {
     initialDataWithFlag = {
@@ -130,6 +131,7 @@ describe('Modals -> WidgetViewerModal', function () {
 
 
   afterEach(() => {
   afterEach(() => {
     MockApiClient.clearMockResponses();
     MockApiClient.clearMockResponses();
+    ProjectsStore.reset();
   });
   });
 
 
   describe('Discover Widgets', function () {
   describe('Discover Widgets', function () {
@@ -525,6 +527,58 @@ describe('Modals -> WidgetViewerModal', function () {
           query: {sort: ['-title']},
           query: {sort: ['-title']},
         });
         });
       });
       });
+
+      it('renders transaction summary link', async function () {
+        ProjectsStore.loadInitialData(initialData.organization.projects);
+        MockApiClient.addMockResponse({
+          url: '/organizations/org-slug/events/',
+          body: {
+            data: [
+              {
+                title: '/organizations/:orgId/dashboards/',
+                transaction: '/discover/homepage/',
+                project: 'project-slug',
+                id: '1',
+              },
+            ],
+            meta: {
+              fields: {
+                title: 'string',
+                transaction: 'string',
+                project: 'string',
+                id: 'string',
+              },
+              isMetricsData: true,
+            },
+          },
+        });
+        mockWidget.queries = [
+          {
+            conditions: 'title:/organizations/:orgId/performance/summary/',
+            fields: [''],
+            aggregates: [''],
+            columns: ['transaction'],
+            name: 'Query Name',
+            orderby: '',
+          },
+        ];
+        await renderModal({
+          initialData: initialDataWithFlag,
+          widget: mockWidget,
+          seriesData: [],
+          seriesResultsType: {'count()': 'duration'},
+        });
+
+        const link = screen.getByTestId('widget-viewer-transaction-link');
+        expect(link).toHaveAttribute(
+          'href',
+          expect.stringMatching(
+            RegExp(
+              '/organizations/org-slug/performance/summary/?.*project=2&referrer=performance-transaction-summary.*transaction=%2.*'
+            )
+          )
+        );
+      });
     });
     });
 
 
     describe('TopN Chart Widget', function () {
     describe('TopN Chart Widget', function () {

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

@@ -48,6 +48,7 @@ import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhan
 import {decodeInteger, decodeList, decodeScalar} from 'sentry/utils/queryString';
 import {decodeInteger, decodeList, decodeScalar} from 'sentry/utils/queryString';
 import useApi from 'sentry/utils/useApi';
 import useApi from 'sentry/utils/useApi';
 import {useLocation} from 'sentry/utils/useLocation';
 import {useLocation} from 'sentry/utils/useLocation';
+import useProjects from 'sentry/utils/useProjects';
 import useRouter from 'sentry/utils/useRouter';
 import useRouter from 'sentry/utils/useRouter';
 import withPageFilters from 'sentry/utils/withPageFilters';
 import withPageFilters from 'sentry/utils/withPageFilters';
 import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboards/types';
 import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboards/types';
@@ -173,6 +174,7 @@ function WidgetViewerModal(props: Props) {
     seriesResultsType,
     seriesResultsType,
   } = props;
   } = props;
   const location = useLocation();
   const location = useLocation();
+  const {projects} = useProjects();
   const router = useRouter();
   const router = useRouter();
   const shouldShowSlider = organization.features.includes('widget-viewer-modal-minimap');
   const shouldShowSlider = organization.features.includes('widget-viewer-modal-minimap');
   // TODO(Tele-Team): Re-enable this when we have a better way to determine if the data is transaction only
   // TODO(Tele-Team): Re-enable this when we have a better way to determine if the data is transaction only
@@ -500,6 +502,8 @@ function WidgetViewerModal(props: Props) {
               location,
               location,
               tableData: tableResults?.[0],
               tableData: tableResults?.[0],
               isFirstPage,
               isFirstPage,
+              projects,
+              eventView,
             }),
             }),
             onResizeColumn,
             onResizeColumn,
           }}
           }}

+ 24 - 1
static/app/components/modals/widgetViewerModal/widgetViewerTableCell.tsx

@@ -8,7 +8,7 @@ import SortLink from 'sentry/components/gridEditable/sortLink';
 import Link from 'sentry/components/links/link';
 import Link from 'sentry/components/links/link';
 import {Tooltip} from 'sentry/components/tooltip';
 import {Tooltip} from 'sentry/components/tooltip';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
-import {Organization, PageFilters} from 'sentry/types';
+import {Organization, PageFilters, Project} from 'sentry/types';
 import {defined} from 'sentry/utils';
 import {defined} from 'sentry/utils';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import {
 import {
@@ -33,8 +33,10 @@ import {
 import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboards/types';
 import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboards/types';
 import {eventViewFromWidget} from 'sentry/views/dashboards/utils';
 import {eventViewFromWidget} from 'sentry/views/dashboards/utils';
 import {ISSUE_FIELDS} from 'sentry/views/dashboards/widgetBuilder/issueWidget/fields';
 import {ISSUE_FIELDS} from 'sentry/views/dashboards/widgetBuilder/issueWidget/fields';
+import {StyledLink} from 'sentry/views/discover/table/tableView';
 import TopResultsIndicator from 'sentry/views/discover/table/topResultsIndicator';
 import TopResultsIndicator from 'sentry/views/discover/table/topResultsIndicator';
 import {TableColumn} from 'sentry/views/discover/table/types';
 import {TableColumn} from 'sentry/views/discover/table/types';
+import {getTargetForTransactionSummaryLink} from 'sentry/views/discover/utils';
 
 
 import {WidgetViewerQueryField} from './utils';
 import {WidgetViewerQueryField} from './utils';
 // Dashboards only supports top 5 for now
 // Dashboards only supports top 5 for now
@@ -45,9 +47,11 @@ type Props = {
   organization: Organization;
   organization: Organization;
   selection: PageFilters;
   selection: PageFilters;
   widget: Widget;
   widget: Widget;
+  eventView?: EventView;
   isFirstPage?: boolean;
   isFirstPage?: boolean;
   isMetricsData?: boolean;
   isMetricsData?: boolean;
   onHeaderClick?: () => void;
   onHeaderClick?: () => void;
+  projects?: Project[];
   tableData?: TableDataWithTitle;
   tableData?: TableDataWithTitle;
 };
 };
 
 
@@ -175,6 +179,8 @@ export const renderGridBodyCell = ({
   widget,
   widget,
   tableData,
   tableData,
   isFirstPage,
   isFirstPage,
+  projects,
+  eventView,
 }: Props) =>
 }: Props) =>
   function (
   function (
     column: GridColumnOrder,
     column: GridColumnOrder,
@@ -222,6 +228,23 @@ export const renderGridBodyCell = ({
         }
         }
         break;
         break;
     }
     }
+
+    if (columnKey === 'transaction' && dataRow.transaction) {
+      cell = (
+        <StyledLink
+          data-test-id="widget-viewer-transaction-link"
+          to={getTargetForTransactionSummaryLink(
+            dataRow,
+            organization,
+            projects,
+            eventView
+          )}
+        >
+          {cell}
+        </StyledLink>
+      );
+    }
+
     const topResultsCount = tableData
     const topResultsCount = tableData
       ? Math.min(tableData?.data.length, DEFAULT_NUM_TOP_EVENTS)
       ? Math.min(tableData?.data.length, DEFAULT_NUM_TOP_EVENTS)
       : DEFAULT_NUM_TOP_EVENTS;
       : DEFAULT_NUM_TOP_EVENTS;

+ 18 - 0
static/app/views/discover/table/tableView.spec.jsx

@@ -317,6 +317,24 @@ describe('TableView > CellActions', function () {
     });
     });
   });
   });
 
 
+  it('renders transaction summary link', function () {
+    rows.data[0].project = 'project-slug';
+
+    renderComponent(initialData, rows, eventView);
+
+    const firstRow = screen.getAllByRole('row')[1];
+    const link = within(firstRow).getByTestId('tableView-transaction-link');
+
+    expect(link).toHaveAttribute(
+      'href',
+      expect.stringMatching(
+        RegExp(
+          '/organizations/org-slug/performance/summary/?.*project=2&referrer=performance-transaction-summary.*transaction=%2.*'
+        )
+      )
+    );
+  });
+
   it('handles go to release', async function () {
   it('handles go to release', async function () {
     renderComponent(initialData, rows, eventView);
     renderComponent(initialData, rows, eventView);
     await openContextMenu(5);
     await openContextMenu(5);

+ 27 - 19
static/app/views/discover/table/tableView.tsx

@@ -49,12 +49,13 @@ import useProjects from 'sentry/utils/useProjects';
 import {useRoutes} from 'sentry/utils/useRoutes';
 import {useRoutes} from 'sentry/utils/useRoutes';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
 import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
-import {
-  generateReplayLink,
-  transactionSummaryRouteWithQuery,
-} from 'sentry/views/performance/transactionSummary/utils';
+import {generateReplayLink} from 'sentry/views/performance/transactionSummary/utils';
 
 
-import {getExpandedResults, pushEventViewToLocation} from '../utils';
+import {
+  getExpandedResults,
+  getTargetForTransactionSummaryLink,
+  pushEventViewToLocation,
+} from '../utils';
 
 
 import {QuickContextHoverWrapper} from './quickContext/quickContextWrapper';
 import {QuickContextHoverWrapper} from './quickContext/quickContextWrapper';
 import {ContextType} from './quickContext/utils';
 import {ContextType} from './quickContext/utils';
@@ -320,6 +321,20 @@ function TableView(props: TableViewProps) {
           {idLink}
           {idLink}
         </QuickContextHoverWrapper>
         </QuickContextHoverWrapper>
       );
       );
+    } else if (columnKey === 'transaction' && dataRow.transaction) {
+      cell = (
+        <StyledLink
+          data-test-id="tableView-transaction-link"
+          to={getTargetForTransactionSummaryLink(
+            dataRow,
+            organization,
+            projects,
+            eventView
+          )}
+        >
+          {cell}
+        </StyledLink>
+      );
     } else if (columnKey === 'trace') {
     } else if (columnKey === 'trace') {
       const dateSelection = eventView.normalizeDateSelection(location);
       const dateSelection = eventView.normalizeDateSelection(location);
       if (dataRow.trace) {
       if (dataRow.trace) {
@@ -468,21 +483,14 @@ function TableView(props: TableViewProps) {
 
 
       switch (action) {
       switch (action) {
         case Actions.TRANSACTION: {
         case Actions.TRANSACTION: {
-          const maybeProject = projects.find(
-            project =>
-              project.slug &&
-              [dataRow['project.name'], dataRow.project].includes(project.slug)
+          const target = getTargetForTransactionSummaryLink(
+            dataRow,
+            organization,
+            projects,
+            nextView
           );
           );
-          const projectID = maybeProject ? [maybeProject.id] : undefined;
-
-          const next = transactionSummaryRouteWithQuery({
-            orgSlug: organization.slug,
-            transaction: String(value),
-            projectID,
-            query: nextView.getPageFiltersQuery(),
-          });
 
 
-          browserHistory.push(normalizeUrl(next));
+          browserHistory.push(normalizeUrl(target));
           return;
           return;
         }
         }
         case Actions.RELEASE: {
         case Actions.RELEASE: {
@@ -642,7 +650,7 @@ const StyledTooltip = styled(Tooltip)`
   max-width: max-content;
   max-width: max-content;
 `;
 `;
 
 
-const StyledLink = styled(Link)`
+export const StyledLink = styled(Link)`
   & div {
   & div {
     display: inline;
     display: inline;
   }
   }

+ 31 - 2
static/app/views/discover/utils.tsx

@@ -7,11 +7,17 @@ import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
 import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
 import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
 import {URL_PARAM} from 'sentry/constants/pageFilters';
 import {URL_PARAM} from 'sentry/constants/pageFilters';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
-import {NewQuery, Organization, OrganizationSummary, SelectValue} from 'sentry/types';
+import {
+  NewQuery,
+  Organization,
+  OrganizationSummary,
+  Project,
+  SelectValue,
+} from 'sentry/types';
 import {Event} from 'sentry/types/event';
 import {Event} from 'sentry/types/event';
 import {getUtcDateString} from 'sentry/utils/dates';
 import {getUtcDateString} from 'sentry/utils/dates';
 import {TableDataRow} from 'sentry/utils/discover/discoverQuery';
 import {TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import EventView, {EventData} from 'sentry/utils/discover/eventView';
 import {
 import {
   aggregateFunctionOutputType,
   aggregateFunctionOutputType,
   Aggregation,
   Aggregation,
@@ -40,6 +46,7 @@ import localStorage from 'sentry/utils/localStorage';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 
 
 import {DashboardWidgetSource, DisplayType, WidgetQuery} from '../dashboards/types';
 import {DashboardWidgetSource, DisplayType, WidgetQuery} from '../dashboards/types';
+import {transactionSummaryRouteWithQuery} from '../performance/transactionSummary/utils';
 
 
 import {displayModeToDisplayType} from './savedQuery/utils';
 import {displayModeToDisplayType} from './savedQuery/utils';
 import {FieldValue, FieldValueKind, TableColumn} from './table/types';
 import {FieldValue, FieldValueKind, TableColumn} from './table/types';
@@ -726,6 +733,28 @@ export function handleAddQueryToDashboard({
   return;
   return;
 }
 }
 
 
+export function getTargetForTransactionSummaryLink(
+  dataRow: EventData,
+  organization: Organization,
+  projects?: Project[],
+  nextView?: EventView
+) {
+  const projectMatch = projects?.find(
+    project =>
+      project.slug && [dataRow['project.name'], dataRow.project].includes(project.slug)
+  );
+  const projectID = projectMatch ? [projectMatch.id] : undefined;
+
+  const target = transactionSummaryRouteWithQuery({
+    orgSlug: organization.slug,
+    transaction: String(dataRow.transaction),
+    projectID,
+    query: nextView?.getPageFiltersQuery() || {},
+  });
+
+  return target;
+}
+
 export function constructAddQueryToDashboardLink({
 export function constructAddQueryToDashboardLink({
   eventView,
   eventView,
   query,
   query,