index.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import {PureComponent} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import {Client} from 'app/api';
  5. import Pagination from 'app/components/pagination';
  6. import {t} from 'app/locale';
  7. import {Organization, TagCollection} from 'app/types';
  8. import {metric} from 'app/utils/analytics';
  9. import {TableData} from 'app/utils/discover/discoverQuery';
  10. import EventView, {isAPIPayloadSimilar} from 'app/utils/discover/eventView';
  11. import Measurements from 'app/utils/measurements/measurements';
  12. import parseLinkHeader from 'app/utils/parseLinkHeader';
  13. import {SPAN_OP_BREAKDOWN_FIELDS} from 'app/utils/performance/spanOperationBreakdowns/constants';
  14. import withApi from 'app/utils/withApi';
  15. import withTags from 'app/utils/withTags';
  16. import TableView from './tableView';
  17. type TableProps = {
  18. api: Client;
  19. location: Location;
  20. eventView: EventView;
  21. organization: Organization;
  22. showTags: boolean;
  23. tags: TagCollection;
  24. setError: (msg: string, code: number) => void;
  25. title: string;
  26. onChangeShowTags: () => void;
  27. confirmedQuery: boolean;
  28. };
  29. type TableState = {
  30. isLoading: boolean;
  31. tableFetchID: symbol | undefined;
  32. error: null | string;
  33. pageLinks: null | string;
  34. tableData: TableData | null | undefined;
  35. };
  36. /**
  37. * `Table` is a container element that handles 2 things
  38. * 1. Fetch data from source
  39. * 2. Handle pagination of data
  40. *
  41. * It will pass the data it fetched to `TableView`, where the state of the
  42. * Table is maintained and controlled
  43. */
  44. class Table extends PureComponent<TableProps, TableState> {
  45. state: TableState = {
  46. isLoading: true,
  47. tableFetchID: undefined,
  48. error: null,
  49. pageLinks: null,
  50. tableData: null,
  51. };
  52. componentDidMount() {
  53. this.fetchData();
  54. }
  55. componentDidUpdate(prevProps: TableProps) {
  56. // Reload data if we aren't already loading, or if we've moved
  57. // from an invalid view state to a valid one.
  58. if (
  59. (!this.state.isLoading && this.shouldRefetchData(prevProps)) ||
  60. (prevProps.eventView.isValid() === false && this.props.eventView.isValid()) ||
  61. prevProps.confirmedQuery !== this.props.confirmedQuery
  62. ) {
  63. this.fetchData();
  64. }
  65. }
  66. shouldRefetchData = (prevProps: TableProps): boolean => {
  67. const thisAPIPayload = this.props.eventView.getEventsAPIPayload(this.props.location);
  68. const otherAPIPayload = prevProps.eventView.getEventsAPIPayload(prevProps.location);
  69. return !isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload);
  70. };
  71. fetchData = () => {
  72. const {eventView, organization, location, setError, confirmedQuery} = this.props;
  73. if (!eventView.isValid() || !confirmedQuery) {
  74. return;
  75. }
  76. // note: If the eventView has no aggregates, the endpoint will automatically add the event id in
  77. // the API payload response
  78. const url = `/organizations/${organization.slug}/eventsv2/`;
  79. const tableFetchID = Symbol('tableFetchID');
  80. const apiPayload = eventView.getEventsAPIPayload(location);
  81. apiPayload.referrer = 'api.discover.query-table';
  82. setError('', 200);
  83. this.setState({isLoading: true, tableFetchID});
  84. metric.mark({name: `discover-events-start-${apiPayload.query}`});
  85. this.props.api.clear();
  86. this.props.api
  87. .requestPromise(url, {
  88. method: 'GET',
  89. includeAllArgs: true,
  90. query: apiPayload,
  91. })
  92. .then(([data, _, jqXHR]) => {
  93. // We want to measure this metric regardless of whether we use the result
  94. metric.measure({
  95. name: 'app.api.discover-query',
  96. start: `discover-events-start-${apiPayload.query}`,
  97. data: {
  98. status: jqXHR && jqXHR.status,
  99. },
  100. });
  101. if (this.state.tableFetchID !== tableFetchID) {
  102. // invariant: a different request was initiated after this request
  103. return;
  104. }
  105. this.setState(prevState => ({
  106. isLoading: false,
  107. tableFetchID: undefined,
  108. error: null,
  109. pageLinks: jqXHR ? jqXHR.getResponseHeader('Link') : prevState.pageLinks,
  110. tableData: data,
  111. }));
  112. })
  113. .catch(err => {
  114. metric.measure({
  115. name: 'app.api.discover-query',
  116. start: `discover-events-start-${apiPayload.query}`,
  117. data: {
  118. status: err.status,
  119. },
  120. });
  121. const message = err?.responseJSON?.detail || t('An unknown error occurred.');
  122. this.setState({
  123. isLoading: false,
  124. tableFetchID: undefined,
  125. error: message,
  126. pageLinks: null,
  127. tableData: null,
  128. });
  129. setError(message, err.status);
  130. });
  131. };
  132. render() {
  133. const {eventView, tags} = this.props;
  134. const {pageLinks, tableData, isLoading, error} = this.state;
  135. const tagKeys = Object.values(tags).map(({key}) => key);
  136. const isFirstPage = pageLinks
  137. ? parseLinkHeader(pageLinks).previous.results === false
  138. : false;
  139. return (
  140. <Container>
  141. <Measurements>
  142. {({measurements}) => {
  143. const measurementKeys = Object.values(measurements).map(({key}) => key);
  144. return (
  145. <TableView
  146. {...this.props}
  147. isLoading={isLoading}
  148. isFirstPage={isFirstPage}
  149. error={error}
  150. eventView={eventView}
  151. tableData={tableData}
  152. tagKeys={tagKeys}
  153. measurementKeys={measurementKeys}
  154. spanOperationBreakdownKeys={SPAN_OP_BREAKDOWN_FIELDS}
  155. />
  156. );
  157. }}
  158. </Measurements>
  159. <Pagination pageLinks={pageLinks} />
  160. </Container>
  161. );
  162. }
  163. }
  164. export default withApi(withTags(Table));
  165. const Container = styled('div')`
  166. min-width: 0;
  167. `;