relatedTransactions.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import GridEditable, {
  5. COL_WIDTH_UNDEFINED,
  6. GridColumn,
  7. } from 'sentry/components/gridEditable';
  8. import type {Alignments} from 'sentry/components/gridEditable/sortLink';
  9. import Link from 'sentry/components/links/link';
  10. import {Organization, Project} from 'sentry/types';
  11. import DiscoverQuery, {
  12. TableData,
  13. TableDataRow,
  14. } from 'sentry/utils/discover/discoverQuery';
  15. import EventView, {EventData} from 'sentry/utils/discover/eventView';
  16. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  17. import {fieldAlignment} from 'sentry/utils/discover/fields';
  18. import type {MetricRule} from 'sentry/views/alerts/rules/metric/types';
  19. import {getMetricRuleDiscoverQuery} from 'sentry/views/alerts/utils/getMetricRuleDiscoverUrl';
  20. import type {TableColumn} from 'sentry/views/eventsV2/table/types';
  21. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  22. import type {TimePeriodType} from './constants';
  23. function getProjectID(eventData: EventData, projects: Project[]): string | undefined {
  24. const projectSlug = (eventData?.project as string) || undefined;
  25. if (typeof projectSlug === undefined) {
  26. return undefined;
  27. }
  28. const project = projects.find(currentProject => currentProject.slug === projectSlug);
  29. if (!project) {
  30. return undefined;
  31. }
  32. return project.id;
  33. }
  34. type TableProps = {
  35. eventView: EventView;
  36. location: Location;
  37. organization: Organization;
  38. projects: Project[];
  39. summaryConditions: string;
  40. };
  41. type TableState = {
  42. widths: number[];
  43. };
  44. class Table extends Component<TableProps, TableState> {
  45. state: TableState = {
  46. widths: [],
  47. };
  48. renderBodyCell(
  49. tableData: TableData | null,
  50. column: TableColumn<keyof TableDataRow>,
  51. dataRow: TableDataRow
  52. ): React.ReactNode {
  53. const {eventView, organization, projects, location, summaryConditions} = this.props;
  54. if (!tableData || !tableData.meta) {
  55. return dataRow[column.key];
  56. }
  57. const tableMeta = tableData.meta;
  58. const field = String(column.key);
  59. const fieldRenderer = getFieldRenderer(field, tableMeta, false);
  60. const rendered = fieldRenderer(dataRow, {organization, location});
  61. if (field === 'transaction') {
  62. const projectID = getProjectID(dataRow, projects);
  63. const summaryView = eventView.clone();
  64. summaryView.query = summaryConditions;
  65. const target = transactionSummaryRouteWithQuery({
  66. orgSlug: organization.slug,
  67. transaction: String(dataRow.transaction) || '',
  68. query: summaryView.generateQueryStringObject(),
  69. projectID,
  70. });
  71. return <Link to={target}>{rendered}</Link>;
  72. }
  73. return rendered;
  74. }
  75. renderBodyCellWithData = (tableData: TableData | null) => {
  76. return (
  77. column: TableColumn<keyof TableDataRow>,
  78. dataRow: TableDataRow
  79. ): React.ReactNode => this.renderBodyCell(tableData, column, dataRow);
  80. };
  81. renderHeadCell(
  82. tableMeta: TableData['meta'],
  83. column: TableColumn<keyof TableDataRow>,
  84. title: React.ReactNode
  85. ): React.ReactNode {
  86. const align = fieldAlignment(column.name, column.type, tableMeta);
  87. const field = {field: column.name, width: column.width};
  88. return <HeaderCell align={align}>{title || field.field}</HeaderCell>;
  89. }
  90. renderHeadCellWithMeta = (tableMeta: TableData['meta'], columnName: string) => {
  91. const columnTitles = ['transactions', 'project', columnName, 'users', 'user misery'];
  92. return (column: TableColumn<keyof TableDataRow>, index: number): React.ReactNode =>
  93. this.renderHeadCell(tableMeta, column, columnTitles[index]);
  94. };
  95. handleResizeColumn = (columnIndex: number, nextColumn: GridColumn) => {
  96. const widths: number[] = [...this.state.widths];
  97. widths[columnIndex] = nextColumn.width
  98. ? Number(nextColumn.width)
  99. : COL_WIDTH_UNDEFINED;
  100. this.setState({widths});
  101. };
  102. getSortedEventView() {
  103. const {eventView} = this.props;
  104. return eventView.withSorts([...eventView.sorts]);
  105. }
  106. render() {
  107. const {eventView, organization, location} = this.props;
  108. const {widths} = this.state;
  109. const columnOrder = eventView
  110. .getColumns()
  111. .map((col: TableColumn<React.ReactText>, i: number) => {
  112. if (typeof widths[i] === 'number') {
  113. return {...col, width: widths[i]};
  114. }
  115. return col;
  116. });
  117. const sortedEventView = this.getSortedEventView();
  118. const columnSortBy = sortedEventView.getSorts();
  119. return (
  120. <Fragment>
  121. <DiscoverQuery
  122. eventView={sortedEventView}
  123. orgSlug={organization.slug}
  124. location={location}
  125. useEvents
  126. >
  127. {({isLoading, tableData}) => (
  128. <GridEditable
  129. isLoading={isLoading}
  130. data={tableData ? tableData.data.slice(0, 5) : []}
  131. columnOrder={columnOrder}
  132. columnSortBy={columnSortBy}
  133. grid={{
  134. onResizeColumn: this.handleResizeColumn,
  135. renderHeadCell: this.renderHeadCellWithMeta(
  136. tableData?.meta,
  137. columnOrder[2].name as string
  138. ) as any,
  139. renderBodyCell: this.renderBodyCellWithData(tableData) as any,
  140. }}
  141. location={location}
  142. />
  143. )}
  144. </DiscoverQuery>
  145. </Fragment>
  146. );
  147. }
  148. }
  149. interface Props {
  150. filter: string;
  151. location: Location;
  152. organization: Organization;
  153. projects: Project[];
  154. rule: MetricRule;
  155. timePeriod: TimePeriodType;
  156. }
  157. function RelatedTransactions({
  158. rule,
  159. projects,
  160. filter,
  161. location,
  162. organization,
  163. timePeriod,
  164. }: Props) {
  165. const eventView = getMetricRuleDiscoverQuery({
  166. rule,
  167. timePeriod,
  168. projects,
  169. });
  170. if (!eventView) {
  171. return null;
  172. }
  173. return (
  174. <Table
  175. eventView={eventView}
  176. projects={projects}
  177. organization={organization}
  178. location={location}
  179. summaryConditions={`${rule.query} ${filter}`}
  180. />
  181. );
  182. }
  183. export default RelatedTransactions;
  184. const HeaderCell = styled('div')<{align: Alignments}>`
  185. display: block;
  186. width: 100%;
  187. white-space: nowrap;
  188. ${(p: {align: Alignments}) => (p.align ? `text-align: ${p.align};` : '')}
  189. `;