genericDiscoverQuery.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import * as React from 'react';
  2. import {Location} from 'history';
  3. import {EventQuery} from 'app/actionCreators/events';
  4. import {Client} from 'app/api';
  5. import {t} from 'app/locale';
  6. import EventView, {
  7. isAPIPayloadSimilar,
  8. LocationQuery,
  9. } from 'app/utils/discover/eventView';
  10. export type GenericChildrenProps<T> = {
  11. isLoading: boolean;
  12. error: null | string;
  13. tableData: T | null;
  14. pageLinks: null | string;
  15. };
  16. export type DiscoverQueryProps = {
  17. api: Client;
  18. /**
  19. * Used as the default source for cursor values.
  20. */
  21. location: Location;
  22. eventView: EventView;
  23. orgSlug: string;
  24. /**
  25. * Record limit to get.
  26. */
  27. limit?: number;
  28. /**
  29. * Explicit cursor value if you aren't using `location.query.cursor` because there are
  30. * multiple paginated results on the page.
  31. */
  32. cursor?: string;
  33. /**
  34. * Include this whenever pagination won't be used. Limit can still be used when this is
  35. * passed, but cursor will be ignored.
  36. */
  37. noPagination?: boolean;
  38. /**
  39. * A callback to set an error so that the error can be rendered in parent components
  40. */
  41. setError?: (msg: string | undefined) => void;
  42. /**
  43. * Sets referrer parameter in the API Payload. Set of allowed referrers are defined
  44. * on the OrganizationEventsV2Endpoint view.
  45. */
  46. referrer?: string;
  47. };
  48. type RequestProps<P> = DiscoverQueryProps & P;
  49. type ReactProps<T> = {
  50. children?: (props: GenericChildrenProps<T>) => React.ReactNode;
  51. };
  52. type Props<T, P> = RequestProps<P> &
  53. ReactProps<T> & {
  54. /**
  55. * Route to the endpoint
  56. */
  57. route: string;
  58. /**
  59. * Allows components to modify the payload before it is set.
  60. */
  61. getRequestPayload?: (props: Props<T, P>) => any;
  62. /**
  63. * An external hook in addition to the event view check to check if data should be refetched
  64. */
  65. shouldRefetchData?: (prevProps: Props<T, P>, props: Props<T, P>) => boolean;
  66. /**
  67. * A hook before fetch that can be used to do things like clearing the api
  68. */
  69. beforeFetch?: (api: Client) => void;
  70. /**
  71. * A hook to modify data into the correct output after data has been received
  72. */
  73. afterFetch?: (data: any, props?: Props<T, P>) => T;
  74. /**
  75. * A hook for parent orchestrators to pass down data based on query results, unlike afterFetch it is not meant for specializations as it will not modify data.
  76. */
  77. didFetch?: (data: T) => void;
  78. };
  79. type State<T> = {
  80. tableFetchID: symbol | undefined;
  81. } & GenericChildrenProps<T>;
  82. /**
  83. * Generic component for discover queries
  84. */
  85. class GenericDiscoverQuery<T, P> extends React.Component<Props<T, P>, State<T>> {
  86. state: State<T> = {
  87. isLoading: true,
  88. tableFetchID: undefined,
  89. error: null,
  90. tableData: null,
  91. pageLinks: null,
  92. };
  93. componentDidMount() {
  94. this.fetchData();
  95. }
  96. componentDidUpdate(prevProps: Props<T, P>) {
  97. // Reload data if we aren't already loading,
  98. const refetchCondition = !this.state.isLoading && this._shouldRefetchData(prevProps);
  99. // or if we've moved from an invalid view state to a valid one,
  100. const eventViewValidation =
  101. prevProps.eventView.isValid() === false && this.props.eventView.isValid();
  102. const shouldRefetchExternal = this.props.shouldRefetchData
  103. ? this.props.shouldRefetchData(prevProps, this.props)
  104. : false;
  105. if (refetchCondition || eventViewValidation || shouldRefetchExternal) {
  106. this.fetchData();
  107. }
  108. }
  109. getPayload(props: Props<T, P>) {
  110. if (this.props.getRequestPayload) {
  111. return this.props.getRequestPayload(props);
  112. }
  113. return props.eventView.getEventsAPIPayload(props.location);
  114. }
  115. _shouldRefetchData = (prevProps: Props<T, P>): boolean => {
  116. const thisAPIPayload = this.getPayload(this.props);
  117. const otherAPIPayload = this.getPayload(prevProps);
  118. return (
  119. !isAPIPayloadSimilar(thisAPIPayload, otherAPIPayload) ||
  120. prevProps.limit !== this.props.limit ||
  121. prevProps.route !== this.props.route ||
  122. prevProps.cursor !== this.props.cursor
  123. );
  124. };
  125. fetchData = async () => {
  126. const {
  127. api,
  128. beforeFetch,
  129. afterFetch,
  130. didFetch,
  131. eventView,
  132. orgSlug,
  133. route,
  134. limit,
  135. cursor,
  136. setError,
  137. noPagination,
  138. referrer,
  139. } = this.props;
  140. if (!eventView.isValid()) {
  141. return;
  142. }
  143. const url = `/organizations/${orgSlug}/${route}/`;
  144. const tableFetchID = Symbol(`tableFetchID`);
  145. const apiPayload: Partial<EventQuery & LocationQuery> = this.getPayload(this.props);
  146. this.setState({isLoading: true, tableFetchID});
  147. setError?.(undefined);
  148. if (limit) {
  149. apiPayload.per_page = limit;
  150. }
  151. if (noPagination) {
  152. apiPayload.noPagination = noPagination;
  153. }
  154. if (cursor) {
  155. apiPayload.cursor = cursor;
  156. }
  157. if (referrer) {
  158. apiPayload.referrer = referrer;
  159. }
  160. beforeFetch?.(api);
  161. try {
  162. const [data, , jqXHR] = await doDiscoverQuery<T>(api, url, apiPayload);
  163. if (this.state.tableFetchID !== tableFetchID) {
  164. // invariant: a different request was initiated after this request
  165. return;
  166. }
  167. const tableData = afterFetch ? afterFetch(data, this.props) : data;
  168. didFetch?.(tableData);
  169. this.setState(prevState => ({
  170. isLoading: false,
  171. tableFetchID: undefined,
  172. error: null,
  173. pageLinks: jqXHR?.getResponseHeader('Link') ?? prevState.pageLinks,
  174. tableData,
  175. }));
  176. } catch (err) {
  177. const error = err?.responseJSON?.detail || t('An unknown error occurred.');
  178. this.setState({
  179. isLoading: false,
  180. tableFetchID: undefined,
  181. error,
  182. tableData: null,
  183. });
  184. if (setError) {
  185. setError(error);
  186. }
  187. }
  188. };
  189. render() {
  190. const {isLoading, error, tableData, pageLinks} = this.state;
  191. const childrenProps: GenericChildrenProps<T> = {
  192. isLoading,
  193. error,
  194. tableData,
  195. pageLinks,
  196. };
  197. const children: ReactProps<T>['children'] = this.props.children; // Explicitly setting type due to issues with generics and React's children
  198. return children?.(childrenProps);
  199. }
  200. }
  201. export type DiscoverQueryRequestParams = Partial<EventQuery & LocationQuery>;
  202. export async function doDiscoverQuery<T>(
  203. api: Client,
  204. url: string,
  205. params: DiscoverQueryRequestParams
  206. ): Promise<[T, string | undefined, JQueryXHR | undefined]> {
  207. return api.requestPromise(url, {
  208. method: 'GET',
  209. includeAllArgs: true,
  210. query: {
  211. // marking params as any so as to not cause typescript errors
  212. ...(params as any),
  213. },
  214. });
  215. }
  216. export default GenericDiscoverQuery;