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 {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
 import {fieldAlignment} from 'sentry/utils/discover/fields';
+import useProjects from 'sentry/utils/useProjects';
 import withOrganization from 'sentry/utils/withOrganization';
+import {StyledLink} from 'sentry/views/discover/table/tableView';
 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 = {
   eventView: EventView;
@@ -57,6 +62,7 @@ function SimpleTableChart({
   fieldAliases,
   loader,
 }: Props) {
+  const {projects} = useProjects();
   function renderRow(
     index: number,
     row: TableDataRow,
@@ -69,12 +75,29 @@ function SimpleTableChart({
         getFieldRenderer(column.key, tableMeta);
 
       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 (
         <TableCell key={`${index}-${columnIndex}:${column.name}`}>
           {topResultsIndicators && columnIndex === 0 && (
             <TopResultsIndicator count={topResultsIndicators} index={index} />
           )}
-          {fieldRenderer(row, {organization, location, eventView, unit})}
+          {cell}
         </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 MemberListStore from 'sentry/stores/memberListStore';
 import PageFiltersStore from 'sentry/stores/pageFiltersStore';
+import ProjectsStore from 'sentry/stores/projectsStore';
 import {space} from 'sentry/styles/space';
 import {Series} from 'sentry/types/echarts';
 import {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
@@ -91,7 +92,7 @@ describe('Modals -> WidgetViewerModal', function () {
         location: {query: {}},
       },
       project: 1,
-      projects: [],
+      projects: [TestStubs.Project()],
     });
 
     initialDataWithFlag = {
@@ -130,6 +131,7 @@ describe('Modals -> WidgetViewerModal', function () {
 
   afterEach(() => {
     MockApiClient.clearMockResponses();
+    ProjectsStore.reset();
   });
 
   describe('Discover Widgets', function () {
@@ -525,6 +527,58 @@ describe('Modals -> WidgetViewerModal', function () {
           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 () {

+ 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 useApi from 'sentry/utils/useApi';
 import {useLocation} from 'sentry/utils/useLocation';
+import useProjects from 'sentry/utils/useProjects';
 import useRouter from 'sentry/utils/useRouter';
 import withPageFilters from 'sentry/utils/withPageFilters';
 import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboards/types';
@@ -173,6 +174,7 @@ function WidgetViewerModal(props: Props) {
     seriesResultsType,
   } = props;
   const location = useLocation();
+  const {projects} = useProjects();
   const router = useRouter();
   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
@@ -500,6 +502,8 @@ function WidgetViewerModal(props: Props) {
               location,
               tableData: tableResults?.[0],
               isFirstPage,
+              projects,
+              eventView,
             }),
             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 {Tooltip} from 'sentry/components/tooltip';
 import {t} from 'sentry/locale';
-import {Organization, PageFilters} from 'sentry/types';
+import {Organization, PageFilters, Project} from 'sentry/types';
 import {defined} from 'sentry/utils';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import {
@@ -33,8 +33,10 @@ import {
 import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboards/types';
 import {eventViewFromWidget} from 'sentry/views/dashboards/utils';
 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 {TableColumn} from 'sentry/views/discover/table/types';
+import {getTargetForTransactionSummaryLink} from 'sentry/views/discover/utils';
 
 import {WidgetViewerQueryField} from './utils';
 // Dashboards only supports top 5 for now
@@ -45,9 +47,11 @@ type Props = {
   organization: Organization;
   selection: PageFilters;
   widget: Widget;
+  eventView?: EventView;
   isFirstPage?: boolean;
   isMetricsData?: boolean;
   onHeaderClick?: () => void;
+  projects?: Project[];
   tableData?: TableDataWithTitle;
 };
 
@@ -175,6 +179,8 @@ export const renderGridBodyCell = ({
   widget,
   tableData,
   isFirstPage,
+  projects,
+  eventView,
 }: Props) =>
   function (
     column: GridColumnOrder,
@@ -222,6 +228,23 @@ export const renderGridBodyCell = ({
         }
         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
       ? Math.min(tableData?.data.length, 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 () {
     renderComponent(initialData, rows, eventView);
     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 {normalizeUrl} from 'sentry/utils/withDomainRequired';
 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 {ContextType} from './quickContext/utils';
@@ -320,6 +321,20 @@ function TableView(props: TableViewProps) {
           {idLink}
         </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') {
       const dateSelection = eventView.normalizeDateSelection(location);
       if (dataRow.trace) {
@@ -468,21 +483,14 @@ function TableView(props: TableViewProps) {
 
       switch (action) {
         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;
         }
         case Actions.RELEASE: {
@@ -642,7 +650,7 @@ const StyledTooltip = styled(Tooltip)`
   max-width: max-content;
 `;
 
-const StyledLink = styled(Link)`
+export const StyledLink = styled(Link)`
   & div {
     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 {URL_PARAM} from 'sentry/constants/pageFilters';
 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 {getUtcDateString} from 'sentry/utils/dates';
 import {TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import EventView, {EventData} from 'sentry/utils/discover/eventView';
 import {
   aggregateFunctionOutputType,
   Aggregation,
@@ -40,6 +46,7 @@ import localStorage from 'sentry/utils/localStorage';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 
 import {DashboardWidgetSource, DisplayType, WidgetQuery} from '../dashboards/types';
+import {transactionSummaryRouteWithQuery} from '../performance/transactionSummary/utils';
 
 import {displayModeToDisplayType} from './savedQuery/utils';
 import {FieldValue, FieldValueKind, TableColumn} from './table/types';
@@ -726,6 +733,28 @@ export function handleAddQueryToDashboard({
   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({
   eventView,
   query,