spanSamplesTable.tsx 5.9 KB

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