spanSamplesTable.tsx 5.4 KB

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