spansTable.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import {Fragment} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {urlEncode} from '@sentry/utils';
  4. import {Location} from 'history';
  5. import GridEditable, {
  6. COL_WIDTH_UNDEFINED,
  7. GridColumnHeader,
  8. } from 'sentry/components/gridEditable';
  9. import SortLink from 'sentry/components/gridEditable/sortLink';
  10. import Link from 'sentry/components/links/link';
  11. import Pagination, {CursorHandler} from 'sentry/components/pagination';
  12. import {Organization} from 'sentry/types';
  13. import {MetaType} from 'sentry/utils/discover/eventView';
  14. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  15. import type {Sort} from 'sentry/utils/discover/fields';
  16. import {decodeScalar} from 'sentry/utils/queryString';
  17. import {useLocation} from 'sentry/utils/useLocation';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import CountCell from 'sentry/views/starfish/components/tableCells/countCell';
  20. import DurationCell from 'sentry/views/starfish/components/tableCells/durationCell';
  21. import ThroughputCell from 'sentry/views/starfish/components/tableCells/throughputCell';
  22. import {TimeSpentCell} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
  23. import {OverflowEllipsisTextContainer} from 'sentry/views/starfish/components/textAlign';
  24. import {useSpanList} from 'sentry/views/starfish/queries/useSpanList';
  25. import {ModuleName, SpanMetricsFields} from 'sentry/views/starfish/types';
  26. import {extractRoute} from 'sentry/views/starfish/utils/extractRoute';
  27. import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
  28. import {DataTitles} from 'sentry/views/starfish/views/spans/types';
  29. type Row = {
  30. 'http_error_count()': number;
  31. 'http_error_count_percent_change()': number;
  32. 'p95(span.self_time)': number;
  33. 'percentile_percent_change(span.self_time, 0.95)': number;
  34. 'span.description': string;
  35. 'span.domain': string;
  36. 'span.group': string;
  37. 'span.op': string;
  38. 'sps()': number;
  39. 'sps_percent_change()': number;
  40. 'time_spent_percentage()': number;
  41. };
  42. type Column = GridColumnHeader<keyof Row>;
  43. type ValidSort = Sort & {
  44. field: keyof Row;
  45. };
  46. type Props = {
  47. moduleName: ModuleName;
  48. sort: ValidSort;
  49. columnOrder?: Column[];
  50. endpoint?: string;
  51. limit?: number;
  52. method?: string;
  53. spanCategory?: string;
  54. };
  55. const {SPAN_SELF_TIME} = SpanMetricsFields;
  56. export const SORTABLE_FIELDS = new Set([
  57. `p95(${SPAN_SELF_TIME})`,
  58. `percentile_percent_change(${SPAN_SELF_TIME}, 0.95)`,
  59. 'sps()',
  60. 'sps_percent_change()',
  61. 'time_spent_percentage()',
  62. ]);
  63. export default function SpansTable({
  64. moduleName,
  65. sort,
  66. columnOrder,
  67. spanCategory,
  68. endpoint,
  69. method,
  70. limit = 25,
  71. }: Props) {
  72. const location = useLocation();
  73. const organization = useOrganization();
  74. const spansCursor = decodeScalar(location.query?.[QueryParameterNames.CURSOR]);
  75. const {isLoading, data, meta, pageLinks} = useSpanList(
  76. moduleName ?? ModuleName.ALL,
  77. undefined,
  78. spanCategory,
  79. [sort],
  80. limit,
  81. 'use-span-list',
  82. spansCursor
  83. );
  84. const handleCursor: CursorHandler = (cursor, pathname, query) => {
  85. browserHistory.push({
  86. pathname,
  87. query: {...query, [QueryParameterNames.CURSOR]: cursor},
  88. });
  89. };
  90. return (
  91. <Fragment>
  92. <GridEditable
  93. isLoading={isLoading}
  94. data={data as Row[]}
  95. columnOrder={columnOrder ?? getColumns(moduleName)}
  96. columnSortBy={[
  97. {
  98. key: sort.field,
  99. order: sort.kind,
  100. },
  101. ]}
  102. grid={{
  103. renderHeadCell: column => renderHeadCell(column, sort, location),
  104. renderBodyCell: (column, row) =>
  105. renderBodyCell(column, row, meta, location, organization, endpoint, method),
  106. }}
  107. location={location}
  108. />
  109. <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
  110. </Fragment>
  111. );
  112. }
  113. function renderHeadCell(column: Column, sort: Sort, location: Location) {
  114. return (
  115. <SortLink
  116. align="left"
  117. canSort={SORTABLE_FIELDS.has(column.key)}
  118. direction={sort.field === column.key ? sort.kind : undefined}
  119. title={column.name}
  120. generateSortLink={() => {
  121. return {
  122. ...location,
  123. query: {
  124. ...location.query,
  125. [QueryParameterNames.SORT]: `-${column.key}`,
  126. },
  127. };
  128. }}
  129. />
  130. );
  131. }
  132. function renderBodyCell(
  133. column: Column,
  134. row: Row,
  135. meta: MetaType | undefined,
  136. location: Location,
  137. organization: Organization,
  138. endpoint?: string,
  139. method?: string
  140. ): React.ReactNode {
  141. if (column.key === 'span.description') {
  142. return (
  143. <OverflowEllipsisTextContainer>
  144. {row['span.group'] ? (
  145. <Link
  146. to={`/starfish/${extractRoute(location)}/span/${row['span.group']}${
  147. endpoint && method ? `?${urlEncode({endpoint, method})}` : ''
  148. }`}
  149. >
  150. {row['span.description'] || '<null>'}
  151. </Link>
  152. ) : (
  153. row['span.description'] || '<null>'
  154. )}
  155. </OverflowEllipsisTextContainer>
  156. );
  157. }
  158. if (column.key === 'time_spent_percentage()') {
  159. return (
  160. <TimeSpentCell
  161. timeSpentPercentage={row['time_spent_percentage()']}
  162. totalSpanTime={row[`sum(${SPAN_SELF_TIME})`]}
  163. />
  164. );
  165. }
  166. if (column.key === 'sps()') {
  167. return <ThroughputCell throughputPerSecond={row['sps()']} />;
  168. }
  169. if (column.key === 'p95(span.self_time)') {
  170. return <DurationCell milliseconds={row['p95(span.self_time)']} />;
  171. }
  172. if (column.key === 'http_error_count()') {
  173. return <CountCell count={row['http_error_count()']} />;
  174. }
  175. if (!meta || !meta?.fields) {
  176. return row[column.key];
  177. }
  178. const renderer = getFieldRenderer(column.key, meta, false);
  179. const rendered = renderer(row, {location, organization});
  180. return rendered;
  181. }
  182. function getDomainHeader(moduleName: ModuleName) {
  183. if (moduleName === ModuleName.HTTP) {
  184. return 'Host';
  185. }
  186. if (moduleName === ModuleName.DB) {
  187. return 'Table';
  188. }
  189. return 'Domain';
  190. }
  191. function getDescriptionHeader(moduleName: ModuleName) {
  192. if (moduleName === ModuleName.HTTP) {
  193. return 'URL';
  194. }
  195. if (moduleName === ModuleName.DB) {
  196. return 'Query';
  197. }
  198. return 'Description';
  199. }
  200. function getColumns(moduleName: ModuleName): Column[] {
  201. const description = getDescriptionHeader(moduleName);
  202. const domain = getDomainHeader(moduleName);
  203. const order: Column[] = [
  204. {
  205. key: 'span.op',
  206. name: 'Operation',
  207. width: 120,
  208. },
  209. {
  210. key: 'span.description',
  211. name: description,
  212. width: COL_WIDTH_UNDEFINED,
  213. },
  214. ...(moduleName !== ModuleName.ALL
  215. ? [
  216. {
  217. key: 'span.domain',
  218. name: domain,
  219. width: COL_WIDTH_UNDEFINED,
  220. } as Column,
  221. ]
  222. : []),
  223. {
  224. key: 'sps()',
  225. name: 'Throughput',
  226. width: COL_WIDTH_UNDEFINED,
  227. },
  228. {
  229. key: 'sps_percent_change()',
  230. name: DataTitles.change,
  231. width: COL_WIDTH_UNDEFINED,
  232. },
  233. {
  234. key: `p95(${SPAN_SELF_TIME})`,
  235. name: DataTitles.p95,
  236. width: COL_WIDTH_UNDEFINED,
  237. },
  238. {
  239. key: `percentile_percent_change(${SPAN_SELF_TIME}, 0.95)`,
  240. name: DataTitles.change,
  241. width: COL_WIDTH_UNDEFINED,
  242. },
  243. ...(moduleName === ModuleName.HTTP
  244. ? [
  245. {
  246. key: 'http_error_count()',
  247. name: DataTitles.errorCount,
  248. width: COL_WIDTH_UNDEFINED,
  249. } as Column,
  250. {
  251. key: 'http_error_count_percent_change()',
  252. name: DataTitles.change,
  253. width: COL_WIDTH_UNDEFINED,
  254. } as Column,
  255. ]
  256. : []),
  257. {
  258. key: 'time_spent_percentage()',
  259. name: DataTitles.timeSpent,
  260. width: COL_WIDTH_UNDEFINED,
  261. },
  262. ];
  263. return order;
  264. }
  265. export function isAValidSort(sort: Sort): sort is ValidSort {
  266. return SORTABLE_FIELDS.has(sort.field);
  267. }