pipelineSpansTable.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import type {Location} from 'history';
  2. import GridEditable, {
  3. COL_WIDTH_UNDEFINED,
  4. type GridColumnHeader,
  5. } from 'sentry/components/gridEditable';
  6. import Link from 'sentry/components/links/link';
  7. import {t} from 'sentry/locale';
  8. import type {Organization} from 'sentry/types/organization';
  9. import EventView, {type EventsMetaType} from 'sentry/utils/discover/eventView';
  10. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  11. import type {Sort} from 'sentry/utils/discover/fields';
  12. import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
  13. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  14. import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
  15. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  16. import {useLocation} from 'sentry/utils/useLocation';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell';
  19. import {useSpansIndexed} from 'sentry/views/starfish/queries/useDiscover';
  20. import {SpanIndexedField} from 'sentry/views/starfish/types';
  21. import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
  22. type Column = GridColumnHeader<
  23. | SpanIndexedField.ID
  24. | SpanIndexedField.SPAN_DURATION
  25. | SpanIndexedField.TIMESTAMP
  26. | SpanIndexedField.USER
  27. >;
  28. const COLUMN_ORDER: Column[] = [
  29. {
  30. key: SpanIndexedField.ID,
  31. name: t('Span ID'),
  32. width: COL_WIDTH_UNDEFINED,
  33. },
  34. {
  35. key: SpanIndexedField.USER,
  36. name: t('User'),
  37. width: COL_WIDTH_UNDEFINED,
  38. },
  39. {
  40. key: SpanIndexedField.TIMESTAMP,
  41. name: t('Timestamp'),
  42. width: COL_WIDTH_UNDEFINED,
  43. },
  44. {
  45. key: SpanIndexedField.SPAN_DURATION,
  46. name: t('Total duration'),
  47. width: 150,
  48. },
  49. ];
  50. const SORTABLE_FIELDS = [
  51. SpanIndexedField.ID,
  52. SpanIndexedField.SPAN_DURATION,
  53. SpanIndexedField.TIMESTAMP,
  54. ];
  55. type ValidSort = Sort & {
  56. field:
  57. | SpanIndexedField.ID
  58. | SpanIndexedField.SPAN_DURATION
  59. | SpanIndexedField.TIMESTAMP;
  60. };
  61. export function isAValidSort(sort: Sort): sort is ValidSort {
  62. return (SORTABLE_FIELDS as unknown as string[]).includes(sort.field);
  63. }
  64. interface Props {
  65. groupId: string;
  66. }
  67. export function PipelineSpansTable({groupId}: Props) {
  68. const location = useLocation();
  69. const organization = useOrganization();
  70. const sortField = decodeScalar(location.query?.[QueryParameterNames.SPANS_SORT]);
  71. let sort = decodeSorts(sortField).filter(isAValidSort)[0];
  72. if (!sort) {
  73. sort = {field: SpanIndexedField.TIMESTAMP, kind: 'desc'};
  74. }
  75. const {
  76. data: rawData,
  77. meta: rawMeta,
  78. error,
  79. isLoading,
  80. } = useSpansIndexed(
  81. {
  82. limit: 30,
  83. sorts: [sort],
  84. fields: [
  85. SpanIndexedField.ID,
  86. SpanIndexedField.TRACE,
  87. SpanIndexedField.SPAN_DURATION,
  88. SpanIndexedField.TRANSACTION_ID,
  89. SpanIndexedField.USER,
  90. SpanIndexedField.TIMESTAMP,
  91. SpanIndexedField.PROJECT,
  92. ],
  93. search: new MutableSearch(`span.category:ai.pipeline span.group:"${groupId}"`),
  94. },
  95. 'api.ai-pipelines.view'
  96. );
  97. const data = rawData || [];
  98. const meta = rawMeta as EventsMetaType;
  99. return (
  100. <VisuallyCompleteWithData
  101. id="PipelineSpansTable"
  102. hasData={data.length > 0}
  103. isLoading={isLoading}
  104. >
  105. <GridEditable
  106. isLoading={isLoading}
  107. error={error}
  108. data={data}
  109. columnOrder={COLUMN_ORDER}
  110. columnSortBy={[
  111. {
  112. key: sort.field,
  113. order: sort.kind,
  114. },
  115. ]}
  116. grid={{
  117. renderHeadCell: column =>
  118. renderHeadCell({
  119. column,
  120. sort,
  121. location,
  122. sortParameterName: QueryParameterNames.SPANS_SORT,
  123. }),
  124. renderBodyCell: (column, row) =>
  125. renderBodyCell(column, row, meta, location, organization),
  126. }}
  127. location={location}
  128. />
  129. </VisuallyCompleteWithData>
  130. );
  131. }
  132. function renderBodyCell(
  133. column: Column,
  134. row: any,
  135. meta: EventsMetaType | undefined,
  136. location: Location,
  137. organization: Organization
  138. ) {
  139. if (column.key === SpanIndexedField.ID) {
  140. if (!row[SpanIndexedField.ID]) {
  141. return <span>(unknown)</span>;
  142. }
  143. if (!row[SpanIndexedField.TRACE]) {
  144. return <span>{row[SpanIndexedField.ID]}</span>;
  145. }
  146. return (
  147. <Link
  148. to={generateLinkToEventInTraceView({
  149. organization,
  150. eventId: row[SpanIndexedField.TRANSACTION_ID],
  151. projectSlug: row[SpanIndexedField.PROJECT],
  152. traceSlug: row[SpanIndexedField.TRACE],
  153. timestamp: row[SpanIndexedField.TIMESTAMP],
  154. location,
  155. eventView: EventView.fromLocation(location),
  156. spanId: row[SpanIndexedField.ID],
  157. })}
  158. >
  159. {row[SpanIndexedField.ID]}
  160. </Link>
  161. );
  162. }
  163. if (!meta || !meta?.fields) {
  164. return row[column.key];
  165. }
  166. const renderer = getFieldRenderer(column.key, meta.fields, false);
  167. const rendered = renderer(row, {
  168. location,
  169. organization,
  170. unit: meta.units?.[column.key],
  171. });
  172. return rendered;
  173. }