spanSamplesTable.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import {Link} from 'react-router';
  2. import styled from '@emotion/styled';
  3. import {LinkButton} from 'sentry/components/button';
  4. import type {GridColumnHeader} from 'sentry/components/gridEditable';
  5. import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  6. import {Tooltip} from 'sentry/components/tooltip';
  7. import {IconProfiling} from 'sentry/icons/iconProfiling';
  8. import {t} from 'sentry/locale';
  9. import EventView from 'sentry/utils/discover/eventView';
  10. import {
  11. generateEventSlug,
  12. generateLinkToEventInTraceView,
  13. } from 'sentry/utils/discover/urls';
  14. import {useLocation} from 'sentry/utils/useLocation';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  17. import {DurationComparisonCell} from 'sentry/views/starfish/components/samplesTable/common';
  18. import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell';
  19. import ResourceSizeCell from 'sentry/views/starfish/components/tableCells/resourceSizeCell';
  20. import {
  21. OverflowEllipsisTextContainer,
  22. TextAlignRight,
  23. } from 'sentry/views/starfish/components/textAlign';
  24. import type {SpanSample} from 'sentry/views/starfish/queries/useSpanSamples';
  25. import {SpanMetricsField} from 'sentry/views/starfish/types';
  26. const {HTTP_RESPONSE_CONTENT_LENGTH} = SpanMetricsField;
  27. type Keys =
  28. | 'transaction_id'
  29. | 'span_id'
  30. | 'profile_id'
  31. | 'timestamp'
  32. | 'duration'
  33. | 'p95_comparison'
  34. | 'avg_comparison'
  35. | 'http.response_content_length';
  36. export type SamplesTableColumnHeader = GridColumnHeader<Keys>;
  37. export const DEFAULT_COLUMN_ORDER: SamplesTableColumnHeader[] = [
  38. {
  39. key: 'span_id',
  40. name: 'Span ID',
  41. width: COL_WIDTH_UNDEFINED,
  42. },
  43. {
  44. key: 'duration',
  45. name: 'Span Duration',
  46. width: COL_WIDTH_UNDEFINED,
  47. },
  48. {
  49. key: 'avg_comparison',
  50. name: 'Compared to Average',
  51. width: COL_WIDTH_UNDEFINED,
  52. },
  53. ];
  54. type SpanTableRow = {
  55. op: string;
  56. transaction: {
  57. id: string;
  58. 'project.name': string;
  59. timestamp: string;
  60. trace: string;
  61. 'transaction.duration': number;
  62. };
  63. } & SpanSample;
  64. type Props = {
  65. avg: number;
  66. data: SpanTableRow[];
  67. isLoading: boolean;
  68. columnOrder?: SamplesTableColumnHeader[];
  69. highlightedSpanId?: string;
  70. onMouseLeaveSample?: () => void;
  71. onMouseOverSample?: (sample: SpanSample) => void;
  72. };
  73. export function SpanSamplesTable({
  74. isLoading,
  75. data,
  76. avg,
  77. highlightedSpanId,
  78. onMouseLeaveSample,
  79. onMouseOverSample,
  80. columnOrder,
  81. }: Props) {
  82. const location = useLocation();
  83. const organization = useOrganization();
  84. function renderHeadCell(column: GridColumnHeader): React.ReactNode {
  85. if (
  86. column.key === 'p95_comparison' ||
  87. column.key === 'avg_comparison' ||
  88. column.key === 'duration'
  89. ) {
  90. return (
  91. <TextAlignRight>
  92. <OverflowEllipsisTextContainer>{column.name}</OverflowEllipsisTextContainer>
  93. </TextAlignRight>
  94. );
  95. }
  96. return <OverflowEllipsisTextContainer>{column.name}</OverflowEllipsisTextContainer>;
  97. }
  98. function renderBodyCell(column: GridColumnHeader, row: SpanTableRow): React.ReactNode {
  99. if (column.key === 'transaction_id') {
  100. return (
  101. <Link
  102. to={generateLinkToEventInTraceView({
  103. eventSlug: generateEventSlug({
  104. id: row['transaction.id'],
  105. project: row.project,
  106. }),
  107. organization,
  108. location,
  109. eventView: EventView.fromLocation(location),
  110. dataRow: {
  111. id: row['transaction.id'],
  112. trace: row.transaction?.trace,
  113. timestamp: row.timestamp,
  114. },
  115. spanId: row.span_id,
  116. })}
  117. >
  118. {row['transaction.id'].slice(0, 8)}
  119. </Link>
  120. );
  121. }
  122. if (column.key === 'span_id') {
  123. return (
  124. <Link
  125. to={generateLinkToEventInTraceView({
  126. eventSlug: generateEventSlug({
  127. id: row['transaction.id'],
  128. project: row.project,
  129. }),
  130. organization,
  131. location,
  132. eventView: EventView.fromLocation(location),
  133. dataRow: {
  134. id: row['transaction.id'],
  135. trace: row.transaction?.trace,
  136. timestamp: row.timestamp,
  137. },
  138. spanId: row.span_id,
  139. })}
  140. >
  141. {row.span_id}
  142. </Link>
  143. );
  144. }
  145. if (column.key === HTTP_RESPONSE_CONTENT_LENGTH) {
  146. const size = parseInt(row[HTTP_RESPONSE_CONTENT_LENGTH], 10);
  147. return <ResourceSizeCell bytes={size} />;
  148. }
  149. if (column.key === 'profile_id') {
  150. return (
  151. <IconWrapper>
  152. {row.profile_id ? (
  153. <Tooltip title={t('View Profile')}>
  154. <LinkButton
  155. to={normalizeUrl(
  156. `/organizations/${organization.slug}/profiling/profile/${row.project}/${row.profile_id}/flamegraph/?spanId=${row.span_id}`
  157. )}
  158. size="xs"
  159. >
  160. <IconProfiling size="xs" />
  161. </LinkButton>
  162. </Tooltip>
  163. ) : (
  164. <div>(no value)</div>
  165. )}
  166. </IconWrapper>
  167. );
  168. }
  169. if (column.key === 'duration') {
  170. return <DurationCell milliseconds={row['span.self_time']} />;
  171. }
  172. if (column.key === 'avg_comparison') {
  173. return (
  174. <DurationComparisonCell
  175. duration={row['span.self_time']}
  176. compareToDuration={avg}
  177. />
  178. );
  179. }
  180. return <span>{row[column.key]}</span>;
  181. }
  182. return (
  183. <GridEditable
  184. isLoading={isLoading}
  185. data={data}
  186. columnOrder={columnOrder ?? DEFAULT_COLUMN_ORDER}
  187. columnSortBy={[]}
  188. onRowMouseOver={onMouseOverSample}
  189. onRowMouseOut={onMouseLeaveSample}
  190. highlightedRowKey={data.findIndex(sample => sample.span_id === highlightedSpanId)}
  191. grid={{
  192. renderHeadCell,
  193. renderBodyCell,
  194. }}
  195. location={location}
  196. />
  197. );
  198. }
  199. const IconWrapper = styled('div')`
  200. text-align: right;
  201. width: 100%;
  202. height: 26px;
  203. `;