spanSamplesTable.tsx 5.3 KB

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