queuesTable.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import qs from 'qs';
  5. import GridEditable, {
  6. COL_WIDTH_UNDEFINED,
  7. type GridColumnHeader,
  8. } from 'sentry/components/gridEditable';
  9. import Link from 'sentry/components/links/link';
  10. import type {CursorHandler} from 'sentry/components/pagination';
  11. import Pagination from 'sentry/components/pagination';
  12. import {t} from 'sentry/locale';
  13. import type {Organization} from 'sentry/types/organization';
  14. import {browserHistory} from 'sentry/utils/browserHistory';
  15. import type {EventsMetaType} from 'sentry/utils/discover/eventView';
  16. import {FIELD_FORMATTERS, getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  17. import type {Sort} from 'sentry/utils/discover/fields';
  18. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import {useQueuesByDestinationQuery} from 'sentry/views/performance/queues/queries/useQueuesByDestinationQuery';
  22. import {Referrer} from 'sentry/views/performance/queues/referrers';
  23. import {useModuleURL} from 'sentry/views/performance/utils/useModuleURL';
  24. import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell';
  25. import {
  26. SpanFunction,
  27. SpanIndexedField,
  28. type SpanMetricsResponse,
  29. } from 'sentry/views/starfish/types';
  30. import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
  31. type Row = Pick<
  32. SpanMetricsResponse,
  33. | 'sum(span.duration)'
  34. | 'messaging.destination.name'
  35. | 'avg(messaging.message.receive.latency)'
  36. | `avg_if(${string},${string},${string})`
  37. | `count_op(${string})`
  38. >;
  39. type Column = GridColumnHeader<string>;
  40. const COLUMN_ORDER: Column[] = [
  41. {
  42. key: 'messaging.destination.name',
  43. name: t('Destination'),
  44. width: COL_WIDTH_UNDEFINED,
  45. },
  46. {
  47. key: 'avg(messaging.message.receive.latency)',
  48. name: t('Avg Time in Queue'),
  49. width: COL_WIDTH_UNDEFINED,
  50. },
  51. {
  52. key: 'avg_if(span.duration,span.op,queue.process)',
  53. name: t('Avg Processing Time'),
  54. width: COL_WIDTH_UNDEFINED,
  55. },
  56. {
  57. key: 'trace_status_rate(ok)',
  58. name: t('Error Rate'),
  59. width: COL_WIDTH_UNDEFINED,
  60. },
  61. {
  62. key: 'count_op(queue.publish)',
  63. name: t('Published'),
  64. width: COL_WIDTH_UNDEFINED,
  65. },
  66. {
  67. key: 'count_op(queue.process)',
  68. name: t('Processed'),
  69. width: COL_WIDTH_UNDEFINED,
  70. },
  71. {
  72. key: 'time_spent_percentage(app,span.duration)',
  73. name: t('Time Spent'),
  74. width: COL_WIDTH_UNDEFINED,
  75. },
  76. ];
  77. const SORTABLE_FIELDS = [
  78. SpanIndexedField.MESSAGING_MESSAGE_DESTINATION_NAME,
  79. 'count_op(queue.publish)',
  80. 'count_op(queue.process)',
  81. 'avg_if(span.duration,span.op,queue.process)',
  82. 'avg(messaging.message.receive.latency)',
  83. `${SpanFunction.TIME_SPENT_PERCENTAGE}(app,span.duration)`,
  84. ] as const;
  85. type ValidSort = Sort & {
  86. field: (typeof SORTABLE_FIELDS)[number];
  87. };
  88. export function isAValidSort(sort: Sort): sort is ValidSort {
  89. return (SORTABLE_FIELDS as ReadonlyArray<string>).includes(sort.field);
  90. }
  91. interface Props {
  92. sort: ValidSort;
  93. destination?: string;
  94. error?: Error | null;
  95. meta?: EventsMetaType;
  96. }
  97. export function QueuesTable({error, destination, sort}: Props) {
  98. const location = useLocation();
  99. const organization = useOrganization();
  100. const {data, isLoading, meta, pageLinks} = useQueuesByDestinationQuery({
  101. destination,
  102. sort,
  103. referrer: Referrer.QUEUES_LANDING_DESTINATIONS_TABLE,
  104. });
  105. const handleCursor: CursorHandler = (newCursor, pathname, query) => {
  106. browserHistory.push({
  107. pathname,
  108. query: {...query, [QueryParameterNames.DESTINATIONS_CURSOR]: newCursor},
  109. });
  110. };
  111. return (
  112. <Fragment>
  113. <GridEditable
  114. aria-label={t('Queues')}
  115. isLoading={isLoading}
  116. error={error}
  117. data={data}
  118. columnOrder={COLUMN_ORDER}
  119. columnSortBy={[
  120. {
  121. key: sort.field,
  122. order: sort.kind,
  123. },
  124. ]}
  125. grid={{
  126. renderHeadCell: column =>
  127. renderHeadCell({
  128. column,
  129. sort,
  130. location,
  131. sortParameterName: QueryParameterNames.DESTINATIONS_SORT,
  132. }),
  133. renderBodyCell: (column, row) =>
  134. renderBodyCell(column, row, meta, location, organization),
  135. }}
  136. location={location}
  137. />
  138. <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
  139. </Fragment>
  140. );
  141. }
  142. function renderBodyCell(
  143. column: Column,
  144. row: Row,
  145. meta: EventsMetaType | undefined,
  146. location: Location,
  147. organization: Organization
  148. ) {
  149. const key = column.key;
  150. if (row[key] === undefined) {
  151. return (
  152. <AlignRight>
  153. <NoValue>{' \u2014 '}</NoValue>
  154. </AlignRight>
  155. );
  156. }
  157. if (key === 'messaging.destination.name' && row[key]) {
  158. return <DestinationCell destination={row[key]} />;
  159. }
  160. if (key.startsWith('count')) {
  161. return <AlignRight>{formatAbbreviatedNumber(row[key])}</AlignRight>;
  162. }
  163. if (key.startsWith('avg')) {
  164. const renderer = FIELD_FORMATTERS.duration.renderFunc;
  165. return renderer(key, row);
  166. }
  167. // Need to invert trace_status_rate(ok) to show error rate
  168. if (key === 'trace_status_rate(ok)') {
  169. const formatter = FIELD_FORMATTERS.percentage.renderFunc;
  170. return (
  171. <AlignRight>
  172. {formatter(key, {'trace_status_rate(ok)': 1 - (row[key] ?? 0)})}
  173. </AlignRight>
  174. );
  175. }
  176. if (!meta?.fields) {
  177. return row[column.key];
  178. }
  179. const renderer = getFieldRenderer(column.key, meta.fields, false);
  180. return renderer(row, {
  181. location,
  182. organization,
  183. unit: meta.units?.[column.key],
  184. });
  185. }
  186. function DestinationCell({destination}: {destination: string}) {
  187. const moduleURL = useModuleURL('queue');
  188. const {query} = useLocation();
  189. const queryString = {
  190. ...query,
  191. destination,
  192. };
  193. return (
  194. <NoOverflow>
  195. <Link to={`${moduleURL}/destination/?${qs.stringify(queryString)}`}>
  196. {destination}
  197. </Link>
  198. </NoOverflow>
  199. );
  200. }
  201. const NoOverflow = styled('span')`
  202. overflow: hidden;
  203. `;
  204. const AlignRight = styled('span')`
  205. text-align: right;
  206. `;
  207. const NoValue = styled('span')`
  208. color: ${p => p.theme.gray300};
  209. `;