resourceSummaryTable.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {GridColumnHeader, GridColumnOrder} from 'sentry/components/gridEditable';
  4. import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  5. import Link from 'sentry/components/links/link';
  6. import type {CursorHandler} from 'sentry/components/pagination';
  7. import Pagination from 'sentry/components/pagination';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {browserHistory} from 'sentry/utils/browserHistory';
  11. import {decodeScalar} from 'sentry/utils/queryString';
  12. import {useLocation} from 'sentry/utils/useLocation';
  13. import {useParams} from 'sentry/utils/useParams';
  14. import {useResourcePagesQuery} from 'sentry/views/insights/browser/resources/queries/useResourcePageQuery';
  15. import {RESOURCE_THROUGHPUT_UNIT} from 'sentry/views/insights/browser/resources/settings';
  16. import {useResourceModuleFilters} from 'sentry/views/insights/browser/resources/utils/useResourceFilters';
  17. import {useResourceSummarySort} from 'sentry/views/insights/browser/resources/utils/useResourceSummarySort';
  18. import {FullSpanDescription} from 'sentry/views/insights/common/components/fullSpanDescription';
  19. import {DurationCell} from 'sentry/views/insights/common/components/tableCells/durationCell';
  20. import {renderHeadCell} from 'sentry/views/insights/common/components/tableCells/renderHeadCell';
  21. import ResourceSizeCell from 'sentry/views/insights/common/components/tableCells/resourceSizeCell';
  22. import {WiderHovercard} from 'sentry/views/insights/common/components/tableCells/spanDescriptionCell';
  23. import {ThroughputCell} from 'sentry/views/insights/common/components/tableCells/throughputCell';
  24. import {QueryParameterNames} from 'sentry/views/insights/common/views/queryParameters';
  25. import {
  26. DataTitles,
  27. getThroughputTitle,
  28. } from 'sentry/views/insights/common/views/spans/types';
  29. import {SpanIndexedField, SpanMetricsField} from 'sentry/views/insights/types';
  30. const {
  31. RESOURCE_RENDER_BLOCKING_STATUS,
  32. SPAN_SELF_TIME,
  33. HTTP_RESPONSE_CONTENT_LENGTH,
  34. TRANSACTION,
  35. USER_GEO_SUBREGION,
  36. } = SpanMetricsField;
  37. type Row = {
  38. 'avg(http.response_content_length)': number;
  39. 'avg(span.self_time)': number;
  40. 'resource.render_blocking_status': '' | 'non-blocking' | 'blocking';
  41. 'spm()': number;
  42. transaction: string;
  43. };
  44. type Column = GridColumnHeader<keyof Row>;
  45. function ResourceSummaryTable() {
  46. const location = useLocation();
  47. const {groupId} = useParams();
  48. const sort = useResourceSummarySort();
  49. const filters = useResourceModuleFilters();
  50. const cursor = decodeScalar(location.query?.[QueryParameterNames.PAGES_CURSOR]);
  51. const {data, isPending, pageLinks} = useResourcePagesQuery(groupId, {
  52. sort,
  53. cursor,
  54. subregions: filters[USER_GEO_SUBREGION],
  55. renderBlockingStatus: filters[RESOURCE_RENDER_BLOCKING_STATUS],
  56. });
  57. const columnOrder: GridColumnOrder<keyof Row>[] = [
  58. {key: 'transaction', width: COL_WIDTH_UNDEFINED, name: 'Found on page'},
  59. {
  60. key: 'spm()',
  61. width: COL_WIDTH_UNDEFINED,
  62. name: getThroughputTitle('http'),
  63. },
  64. {
  65. key: `avg(${SPAN_SELF_TIME})`,
  66. width: COL_WIDTH_UNDEFINED,
  67. name: t('Avg Duration'),
  68. },
  69. {
  70. key: `avg(${HTTP_RESPONSE_CONTENT_LENGTH})`,
  71. width: COL_WIDTH_UNDEFINED,
  72. name: DataTitles[`avg(${HTTP_RESPONSE_CONTENT_LENGTH})`],
  73. },
  74. {
  75. key: RESOURCE_RENDER_BLOCKING_STATUS,
  76. width: COL_WIDTH_UNDEFINED,
  77. name: t('Render Blocking'),
  78. },
  79. ];
  80. const renderBodyCell = (col: Column, row: Row) => {
  81. const {key} = col;
  82. if (key === 'spm()') {
  83. return <ThroughputCell rate={row[key]} unit={RESOURCE_THROUGHPUT_UNIT} />;
  84. }
  85. if (key === 'avg(span.self_time)') {
  86. return <DurationCell milliseconds={row[key]} />;
  87. }
  88. if (key === 'avg(http.response_content_length)') {
  89. return <ResourceSizeCell bytes={row[key]} />;
  90. }
  91. if (key === 'transaction') {
  92. const blockingStatus = row['resource.render_blocking_status'];
  93. let query = `!has:${RESOURCE_RENDER_BLOCKING_STATUS}`;
  94. if (blockingStatus) {
  95. query = `${RESOURCE_RENDER_BLOCKING_STATUS}:${blockingStatus}`;
  96. }
  97. const link = (
  98. <Link
  99. to={{
  100. pathname: location.pathname,
  101. query: {
  102. ...location.query,
  103. transaction: row[key],
  104. query: [query],
  105. },
  106. }}
  107. >
  108. {row[key]}
  109. </Link>
  110. );
  111. return (
  112. <DescriptionWrapper>
  113. <WiderHovercard
  114. position="right"
  115. body={
  116. <Fragment>
  117. <TitleWrapper>{t('Example')}</TitleWrapper>
  118. <FullSpanDescription
  119. group={groupId}
  120. language="http"
  121. filters={{
  122. [SpanIndexedField.RESOURCE_RENDER_BLOCKING_STATUS]:
  123. row[RESOURCE_RENDER_BLOCKING_STATUS],
  124. [SpanIndexedField.TRANSACTION]: row[TRANSACTION],
  125. }}
  126. />
  127. </Fragment>
  128. }
  129. >
  130. {link}
  131. </WiderHovercard>
  132. </DescriptionWrapper>
  133. );
  134. }
  135. if (key === RESOURCE_RENDER_BLOCKING_STATUS) {
  136. const value = row[key];
  137. if (value === 'blocking') {
  138. return <span>{t('Yes')}</span>;
  139. }
  140. if (value === 'non-blocking') {
  141. return <span>{t('No')}</span>;
  142. }
  143. return <span>{'-'}</span>;
  144. }
  145. return <span>{row[key]}</span>;
  146. };
  147. const handleCursor: CursorHandler = (newCursor, pathname, query) => {
  148. browserHistory.push({
  149. pathname,
  150. query: {...query, [QueryParameterNames.PAGES_CURSOR]: newCursor},
  151. });
  152. };
  153. return (
  154. <Fragment>
  155. <GridEditable
  156. data={data || []}
  157. isLoading={isPending}
  158. columnOrder={columnOrder}
  159. columnSortBy={[
  160. {
  161. key: sort.field,
  162. order: sort.kind,
  163. },
  164. ]}
  165. grid={{
  166. renderHeadCell: column =>
  167. renderHeadCell({
  168. column,
  169. location,
  170. sort,
  171. }),
  172. renderBodyCell,
  173. }}
  174. />
  175. <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
  176. </Fragment>
  177. );
  178. }
  179. export const getActionName = (transactionOp: string) => {
  180. switch (transactionOp) {
  181. case 'ui.action.click':
  182. return 'Click';
  183. default:
  184. return transactionOp;
  185. }
  186. };
  187. const TitleWrapper = styled('div')`
  188. margin-bottom: ${space(1)};
  189. `;
  190. const DescriptionWrapper = styled('div')`
  191. .inline-flex {
  192. display: inline-flex;
  193. }
  194. `;
  195. export default ResourceSummaryTable;