index.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import {PureComponent} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import {EventQuery} from 'sentry/actionCreators/events';
  5. import {Client} from 'sentry/api';
  6. import Pagination, {CursorHandler} from 'sentry/components/pagination';
  7. import {t} from 'sentry/locale';
  8. import {Organization} from 'sentry/types';
  9. import {metric} from 'sentry/utils/analytics';
  10. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  11. import {CustomMeasurementsContext} from 'sentry/utils/customMeasurements/customMeasurementsContext';
  12. import {TableData} from 'sentry/utils/discover/discoverQuery';
  13. import EventView, {
  14. isAPIPayloadSimilar,
  15. LocationQuery,
  16. } from 'sentry/utils/discover/eventView';
  17. import {SPAN_OP_BREAKDOWN_FIELDS} from 'sentry/utils/discover/fields';
  18. import Measurements from 'sentry/utils/measurements/measurements';
  19. import parseLinkHeader from 'sentry/utils/parseLinkHeader';
  20. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  21. import withApi from 'sentry/utils/withApi';
  22. import TableView from './tableView';
  23. type TableProps = {
  24. api: Client;
  25. confirmedQuery: boolean;
  26. eventView: EventView;
  27. location: Location;
  28. onChangeShowTags: () => void;
  29. onCursor: CursorHandler;
  30. organization: Organization;
  31. setError: (msg: string, code: number) => void;
  32. showTags: boolean;
  33. title: string;
  34. isHomepage?: boolean;
  35. setTips?: (tips: string[]) => void;
  36. };
  37. type TableState = {
  38. error: null | string;
  39. isLoading: boolean;
  40. pageLinks: null | string;
  41. prevView: null | EventView;
  42. tableData: TableData | null | undefined;
  43. tableFetchID: symbol | undefined;
  44. };
  45. /**
  46. * `Table` is a container element that handles 2 things
  47. * 1. Fetch data from source
  48. * 2. Handle pagination of data
  49. *
  50. * It will pass the data it fetched to `TableView`, where the state of the
  51. * Table is maintained and controlled
  52. */
  53. class Table extends PureComponent<TableProps, TableState> {
  54. state: TableState = {
  55. isLoading: true,
  56. tableFetchID: undefined,
  57. error: null,
  58. pageLinks: null,
  59. tableData: null,
  60. prevView: null,
  61. };
  62. componentDidMount() {
  63. this.fetchData();
  64. }
  65. componentDidUpdate(prevProps: TableProps) {
  66. // Reload data if we aren't already loading, or if we've moved
  67. // from an invalid view state to a valid one.
  68. if (
  69. (!this.state.isLoading && this.shouldRefetchData(prevProps)) ||
  70. (prevProps.eventView.isValid() === false && this.props.eventView.isValid()) ||
  71. (prevProps.confirmedQuery !== this.props.confirmedQuery && this.didViewChange())
  72. ) {
  73. this.fetchData();
  74. }
  75. }
  76. didViewChange = (): boolean => {
  77. const {prevView} = this.state;
  78. const thisAPIPayload = this.props.eventView.getEventsAPIPayload(this.props.location);
  79. if (prevView === null) {
  80. return true;
  81. }
  82. const otherAPIPayload = prevView.getEventsAPIPayload(this.props.location);
  83. return !isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  84. };
  85. shouldRefetchData = (prevProps: TableProps): boolean => {
  86. const thisAPIPayload = this.props.eventView.getEventsAPIPayload(this.props.location);
  87. const otherAPIPayload = prevProps.eventView.getEventsAPIPayload(prevProps.location);
  88. return !isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  89. };
  90. fetchData = () => {
  91. const {eventView, organization, location, setError, confirmedQuery, setTips} =
  92. this.props;
  93. if (!eventView.isValid() || !confirmedQuery) {
  94. return;
  95. }
  96. this.setState({prevView: eventView});
  97. // note: If the eventView has no aggregates, the endpoint will automatically add the event id in
  98. // the API payload response
  99. const url = `/organizations/${organization.slug}/events/`;
  100. const tableFetchID = Symbol('tableFetchID');
  101. const apiPayload = eventView.getEventsAPIPayload(location) as LocationQuery &
  102. EventQuery;
  103. apiPayload.referrer = 'api.discover.query-table';
  104. setError('', 200);
  105. this.setState({isLoading: true, tableFetchID});
  106. metric.mark({name: `discover-events-start-${apiPayload.query}`});
  107. this.props.api.clear();
  108. this.props.api
  109. .requestPromise(url, {
  110. method: 'GET',
  111. includeAllArgs: true,
  112. query: apiPayload,
  113. })
  114. .then(([data, _, resp]) => {
  115. // We want to measure this metric regardless of whether we use the result
  116. metric.measure({
  117. name: 'app.api.discover-query',
  118. start: `discover-events-start-${apiPayload.query}`,
  119. data: {
  120. status: resp && resp.status,
  121. },
  122. });
  123. if (this.state.tableFetchID !== tableFetchID) {
  124. // invariant: a different request was initiated after this request
  125. return;
  126. }
  127. const {fields, ...nonFieldsMeta} = data.meta ?? {};
  128. // events api uses a different response format so we need to construct tableData differently
  129. const tableData = {
  130. ...data,
  131. meta: {...fields, ...nonFieldsMeta},
  132. };
  133. this.setState(prevState => ({
  134. isLoading: false,
  135. tableFetchID: undefined,
  136. error: null,
  137. pageLinks: resp ? resp.getResponseHeader('Link') : prevState.pageLinks,
  138. tableData,
  139. }));
  140. const tips: string[] = [];
  141. const {query, columns} = tableData?.meta?.tips ?? {};
  142. if (query) {
  143. tips.push(query);
  144. }
  145. if (columns) {
  146. tips.push(columns);
  147. }
  148. setTips?.(tips);
  149. })
  150. .catch(err => {
  151. metric.measure({
  152. name: 'app.api.discover-query',
  153. start: `discover-events-start-${apiPayload.query}`,
  154. data: {
  155. status: err.status,
  156. },
  157. });
  158. const message = err?.responseJSON?.detail || t('An unknown error occurred.');
  159. this.setState({
  160. isLoading: false,
  161. tableFetchID: undefined,
  162. error: message,
  163. pageLinks: null,
  164. tableData: null,
  165. });
  166. trackAdvancedAnalyticsEvent('discover_search.failed', {
  167. organization: this.props.organization,
  168. search_type: 'events',
  169. search_source: 'discover_search',
  170. error: message,
  171. });
  172. setError(message, err.status);
  173. });
  174. };
  175. render() {
  176. const {eventView, onCursor} = this.props;
  177. const {pageLinks, tableData, isLoading, error} = this.state;
  178. const isFirstPage = pageLinks
  179. ? parseLinkHeader(pageLinks).previous.results === false
  180. : false;
  181. return (
  182. <Container>
  183. <Measurements>
  184. {({measurements}) => {
  185. const measurementKeys = Object.values(measurements).map(({key}) => key);
  186. return (
  187. <CustomMeasurementsContext.Consumer>
  188. {contextValue => (
  189. <VisuallyCompleteWithData
  190. id="Discover-Table"
  191. hasData={(tableData?.data?.length ?? 0) > 0}
  192. >
  193. <TableView
  194. {...this.props}
  195. isLoading={isLoading}
  196. isFirstPage={isFirstPage}
  197. error={error}
  198. eventView={eventView}
  199. tableData={tableData}
  200. measurementKeys={measurementKeys}
  201. spanOperationBreakdownKeys={SPAN_OP_BREAKDOWN_FIELDS}
  202. customMeasurements={contextValue?.customMeasurements ?? undefined}
  203. />
  204. </VisuallyCompleteWithData>
  205. )}
  206. </CustomMeasurementsContext.Consumer>
  207. );
  208. }}
  209. </Measurements>
  210. <Pagination pageLinks={pageLinks} onCursor={onCursor} />
  211. </Container>
  212. );
  213. }
  214. }
  215. export default withApi(Table);
  216. const Container = styled('div')`
  217. min-width: 0;
  218. `;