eventsTable.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. import {Component, Fragment} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location, LocationDescriptorObject} from 'history';
  5. import GridEditable, {
  6. COL_WIDTH_UNDEFINED,
  7. GridColumn,
  8. } from 'sentry/components/gridEditable';
  9. import SortLink from 'sentry/components/gridEditable/sortLink';
  10. import Link from 'sentry/components/links/link';
  11. import Pagination from 'sentry/components/pagination';
  12. import QuestionTooltip from 'sentry/components/questionTooltip';
  13. import Tooltip from 'sentry/components/tooltip';
  14. import {t, tct} from 'sentry/locale';
  15. import {Organization, Project} from 'sentry/types';
  16. import {defined} from 'sentry/utils';
  17. import {trackAnalyticsEvent} from 'sentry/utils/analytics';
  18. import DiscoverQuery, {
  19. TableData,
  20. TableDataRow,
  21. } from 'sentry/utils/discover/discoverQuery';
  22. import EventView, {EventData, isFieldSortable} from 'sentry/utils/discover/eventView';
  23. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  24. import {
  25. fieldAlignment,
  26. getAggregateAlias,
  27. isSpanOperationBreakdownField,
  28. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  29. } from 'sentry/utils/discover/fields';
  30. import parseLinkHeader from 'sentry/utils/parseLinkHeader';
  31. import CellAction, {Actions, updateQuery} from 'sentry/views/eventsV2/table/cellAction';
  32. import {TableColumn} from 'sentry/views/eventsV2/table/types';
  33. import {COLUMN_TITLES} from '../../data';
  34. import {
  35. generateTraceLink,
  36. generateTransactionLink,
  37. normalizeSearchConditions,
  38. } from '../utils';
  39. import OperationSort, {TitleProps} from './operationSort';
  40. export function getProjectID(
  41. eventData: EventData,
  42. projects: Project[]
  43. ): string | undefined {
  44. const projectSlug = (eventData?.project as string) || undefined;
  45. if (typeof projectSlug === undefined) {
  46. return undefined;
  47. }
  48. const project = projects.find(currentProject => currentProject.slug === projectSlug);
  49. if (!project) {
  50. return undefined;
  51. }
  52. return project.id;
  53. }
  54. class OperationTitle extends Component<TitleProps> {
  55. render() {
  56. const {onClick} = this.props;
  57. return (
  58. <div onClick={onClick}>
  59. <span>{t('operation duration')}</span>
  60. <StyledIconQuestion
  61. size="xs"
  62. position="top"
  63. title={t(
  64. `Span durations are summed over the course of an entire transaction. Any overlapping spans are only counted once.`
  65. )}
  66. />
  67. </div>
  68. );
  69. }
  70. }
  71. type Props = {
  72. eventView: EventView;
  73. location: Location;
  74. organization: Organization;
  75. setError: (msg: string | undefined) => void;
  76. totalEventCount: string;
  77. transactionName: string;
  78. columnTitles?: string[];
  79. };
  80. type State = {
  81. widths: number[];
  82. };
  83. class EventsTable extends Component<Props, State> {
  84. state: State = {
  85. widths: [],
  86. };
  87. handleCellAction = (column: TableColumn<keyof TableDataRow>) => {
  88. return (action: Actions, value: React.ReactText) => {
  89. const {eventView, location, organization} = this.props;
  90. trackAnalyticsEvent({
  91. eventKey: 'performance_views.transactionEvents.cellaction',
  92. eventName: 'Performance Views: Transaction Events Tab Cell Action Clicked',
  93. organization_id: parseInt(organization.id, 10),
  94. action,
  95. });
  96. const searchConditions = normalizeSearchConditions(eventView.query);
  97. updateQuery(searchConditions, action, column, value);
  98. browserHistory.push({
  99. pathname: location.pathname,
  100. query: {
  101. ...location.query,
  102. cursor: undefined,
  103. query: searchConditions.formatString(),
  104. },
  105. });
  106. };
  107. };
  108. renderBodyCell(
  109. tableData: TableData | null,
  110. column: TableColumn<keyof TableDataRow>,
  111. dataRow: TableDataRow
  112. ): React.ReactNode {
  113. const {eventView, organization, location, transactionName} = this.props;
  114. if (!tableData || !tableData.meta) {
  115. return dataRow[column.key];
  116. }
  117. const tableMeta = tableData.meta;
  118. const field = String(column.key);
  119. const fieldRenderer = getFieldRenderer(field, tableMeta);
  120. const rendered = fieldRenderer(dataRow, {organization, location, eventView});
  121. const allowActions = [
  122. Actions.ADD,
  123. Actions.EXCLUDE,
  124. Actions.SHOW_GREATER_THAN,
  125. Actions.SHOW_LESS_THAN,
  126. ];
  127. if (field === 'id' || field === 'trace') {
  128. const generateLink = field === 'id' ? generateTransactionLink : generateTraceLink;
  129. const target = generateLink(transactionName)(organization, dataRow, location.query);
  130. return (
  131. <CellAction
  132. column={column}
  133. dataRow={dataRow}
  134. handleCellAction={this.handleCellAction(column)}
  135. allowActions={allowActions}
  136. >
  137. <Link to={target}>{rendered}</Link>
  138. </CellAction>
  139. );
  140. }
  141. const fieldName = getAggregateAlias(field);
  142. const value = dataRow[fieldName];
  143. if (tableMeta[fieldName] === 'integer' && defined(value) && value > 999) {
  144. return (
  145. <Tooltip
  146. title={value.toLocaleString()}
  147. containerDisplayMode="block"
  148. position="right"
  149. >
  150. <CellAction
  151. column={column}
  152. dataRow={dataRow}
  153. handleCellAction={this.handleCellAction(column)}
  154. allowActions={allowActions}
  155. >
  156. {rendered}
  157. </CellAction>
  158. </Tooltip>
  159. );
  160. }
  161. return (
  162. <CellAction
  163. column={column}
  164. dataRow={dataRow}
  165. handleCellAction={this.handleCellAction(column)}
  166. allowActions={allowActions}
  167. >
  168. {rendered}
  169. </CellAction>
  170. );
  171. }
  172. renderBodyCellWithData = (tableData: TableData | null) => {
  173. return (
  174. column: TableColumn<keyof TableDataRow>,
  175. dataRow: TableDataRow
  176. ): React.ReactNode => this.renderBodyCell(tableData, column, dataRow);
  177. };
  178. onSortClick(currentSortKind?: string, currentSortField?: string) {
  179. const {organization} = this.props;
  180. trackAnalyticsEvent({
  181. eventKey: 'performance_views.transactionEvents.sort',
  182. eventName: 'Performance Views: Transaction Events Tab Sorted',
  183. organization_id: parseInt(organization.id, 10),
  184. field: currentSortField,
  185. direction: currentSortKind,
  186. });
  187. }
  188. renderHeadCell(
  189. tableMeta: TableData['meta'],
  190. column: TableColumn<keyof TableDataRow>,
  191. title: React.ReactNode
  192. ): React.ReactNode {
  193. const {eventView, location} = this.props;
  194. const align = fieldAlignment(column.name, column.type, tableMeta);
  195. const field = {field: column.name, width: column.width};
  196. function generateSortLink(): LocationDescriptorObject | undefined {
  197. if (!tableMeta) {
  198. return undefined;
  199. }
  200. const nextEventView = eventView.sortOnField(field, tableMeta);
  201. const queryStringObject = nextEventView.generateQueryStringObject();
  202. return {
  203. ...location,
  204. query: {...location.query, sort: queryStringObject.sort},
  205. };
  206. }
  207. const currentSort = eventView.sortForField(field, tableMeta);
  208. // Event id and Trace id are technically sortable but we don't want to sort them here since sorting by a uuid value doesn't make sense
  209. const canSort =
  210. field.field !== 'id' &&
  211. field.field !== 'trace' &&
  212. field.field !== SPAN_OP_RELATIVE_BREAKDOWN_FIELD &&
  213. isFieldSortable(field, tableMeta);
  214. const currentSortKind = currentSort ? currentSort.kind : undefined;
  215. const currentSortField = currentSort ? currentSort.field : undefined;
  216. if (field.field === SPAN_OP_RELATIVE_BREAKDOWN_FIELD) {
  217. title = (
  218. <OperationSort
  219. title={OperationTitle}
  220. eventView={eventView}
  221. tableMeta={tableMeta}
  222. location={location}
  223. />
  224. );
  225. }
  226. const sortLink = (
  227. <SortLink
  228. align={align}
  229. title={title || field.field}
  230. direction={currentSortKind}
  231. canSort={canSort}
  232. generateSortLink={generateSortLink}
  233. onClick={() => this.onSortClick(currentSortKind, currentSortField)}
  234. />
  235. );
  236. return sortLink;
  237. }
  238. renderHeadCellWithMeta = (tableMeta: TableData['meta']) => {
  239. const columnTitles = this.props.columnTitles ?? COLUMN_TITLES;
  240. return (column: TableColumn<keyof TableDataRow>, index: number): React.ReactNode =>
  241. this.renderHeadCell(tableMeta, column, columnTitles[index]);
  242. };
  243. handleResizeColumn = (columnIndex: number, nextColumn: GridColumn) => {
  244. const widths: number[] = [...this.state.widths];
  245. widths[columnIndex] = nextColumn.width
  246. ? Number(nextColumn.width)
  247. : COL_WIDTH_UNDEFINED;
  248. this.setState({widths});
  249. };
  250. render() {
  251. const {eventView, organization, location, setError, totalEventCount} = this.props;
  252. const totalTransactionsView = eventView.clone();
  253. totalTransactionsView.sorts = [];
  254. totalTransactionsView.fields = [{field: 'count()', width: -1}];
  255. const {widths} = this.state;
  256. const containsSpanOpsBreakdown = eventView
  257. .getColumns()
  258. .find(
  259. (col: TableColumn<React.ReactText>) =>
  260. col.name === SPAN_OP_RELATIVE_BREAKDOWN_FIELD
  261. );
  262. const columnOrder = eventView
  263. .getColumns()
  264. .filter(
  265. (col: TableColumn<React.ReactText>) =>
  266. !containsSpanOpsBreakdown || !isSpanOperationBreakdownField(col.name)
  267. )
  268. .map((col: TableColumn<React.ReactText>, i: number) => {
  269. if (typeof widths[i] === 'number') {
  270. return {...col, width: widths[i]};
  271. }
  272. return col;
  273. });
  274. return (
  275. <div>
  276. <DiscoverQuery
  277. eventView={eventView}
  278. orgSlug={organization.slug}
  279. location={location}
  280. setError={error => setError(error?.message)}
  281. referrer="api.performance.transaction-events"
  282. useEvents
  283. >
  284. {({pageLinks, isLoading, tableData}) => {
  285. const parsedPageLinks = parseLinkHeader(pageLinks);
  286. let currentEvent = parsedPageLinks?.next?.cursor.split(':')[1] ?? 0;
  287. if (!parsedPageLinks?.next?.results) {
  288. currentEvent = totalEventCount;
  289. }
  290. const paginationCaption =
  291. totalEventCount && currentEvent
  292. ? tct('Showing [currentEvent] of [totalEventCount] events', {
  293. currentEvent,
  294. totalEventCount,
  295. })
  296. : undefined;
  297. return (
  298. <Fragment>
  299. <GridEditable
  300. isLoading={isLoading}
  301. data={tableData?.data ?? []}
  302. columnOrder={columnOrder}
  303. columnSortBy={eventView.getSorts()}
  304. grid={{
  305. onResizeColumn: this.handleResizeColumn,
  306. renderHeadCell: this.renderHeadCellWithMeta(tableData?.meta) as any,
  307. renderBodyCell: this.renderBodyCellWithData(tableData) as any,
  308. }}
  309. location={location}
  310. />
  311. <Pagination
  312. disabled={isLoading}
  313. caption={paginationCaption}
  314. pageLinks={pageLinks}
  315. />
  316. </Fragment>
  317. );
  318. }}
  319. </DiscoverQuery>
  320. </div>
  321. );
  322. }
  323. }
  324. const StyledIconQuestion = styled(QuestionTooltip)`
  325. position: relative;
  326. top: 1px;
  327. left: 4px;
  328. `;
  329. export default EventsTable;