metricsWidgetQueries.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import * as React from 'react';
  2. import cloneDeep from 'lodash/cloneDeep';
  3. import isEqual from 'lodash/isEqual';
  4. import omit from 'lodash/omit';
  5. import {doMetricsRequest} from 'sentry/actionCreators/metrics';
  6. import {Client} from 'sentry/api';
  7. import {isSelectionEqual} from 'sentry/components/organizations/pageFilters/utils';
  8. import {t} from 'sentry/locale';
  9. import {MetricsApiResponse, OrganizationSummary, PageFilters} from 'sentry/types';
  10. import {Series} from 'sentry/types/echarts';
  11. import {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
  12. import {TOP_N} from 'sentry/utils/discover/types';
  13. import {transformMetricsResponseToSeries} from 'sentry/utils/metrics/transformMetricsResponseToSeries';
  14. import {DisplayType, Widget} from '../types';
  15. import {getWidgetInterval} from '../utils';
  16. type Props = {
  17. api: Client;
  18. children: (
  19. props: Pick<State, 'loading' | 'timeseriesResults' | 'tableResults' | 'errorMessage'>
  20. ) => React.ReactNode;
  21. organization: OrganizationSummary;
  22. selection: PageFilters;
  23. widget: Widget;
  24. limit?: number;
  25. };
  26. type State = {
  27. loading: boolean;
  28. errorMessage?: string;
  29. queryFetchID?: symbol;
  30. rawResults?: MetricsApiResponse[];
  31. tableResults?: TableDataWithTitle[];
  32. timeseriesResults?: Series[];
  33. };
  34. class MetricsWidgetQueries extends React.Component<Props, State> {
  35. state: State = {
  36. loading: true,
  37. queryFetchID: undefined,
  38. errorMessage: undefined,
  39. timeseriesResults: undefined,
  40. rawResults: undefined,
  41. tableResults: undefined,
  42. };
  43. componentDidMount() {
  44. this._isMounted = true;
  45. this.fetchData();
  46. }
  47. componentDidUpdate(prevProps: Props) {
  48. const {loading, rawResults} = this.state;
  49. const {selection, widget, organization, limit} = this.props;
  50. const ignroredWidgetProps = [
  51. 'queries',
  52. 'title',
  53. 'id',
  54. 'layout',
  55. 'tempId',
  56. 'widgetType',
  57. ];
  58. const ignoredQueryProps = ['name', 'fields'];
  59. const widgetQueryNames = widget.queries.map(q => q.name);
  60. const prevWidgetQueryNames = prevProps.widget.queries.map(q => q.name);
  61. if (
  62. limit !== prevProps.limit ||
  63. organization.slug !== prevProps.organization.slug ||
  64. !isSelectionEqual(selection, prevProps.selection) ||
  65. // If the widget changed (ignore unimportant fields, + queries as they are handled lower)
  66. !isEqual(
  67. omit(widget, ignroredWidgetProps),
  68. omit(prevProps.widget, ignroredWidgetProps)
  69. ) ||
  70. // If the queries changed (ignore unimportant name, + fields as they are handled lower)
  71. !isEqual(
  72. widget.queries.map(q => omit(q, ignoredQueryProps)),
  73. prevProps.widget.queries.map(q => omit(q, ignoredQueryProps))
  74. ) ||
  75. // If the fields changed (ignore falsy/empty fields -> they can happen after clicking on Add Overlay)
  76. !isEqual(
  77. widget.queries.flatMap(q => q.fields.filter(field => !!field)),
  78. prevProps.widget.queries.flatMap(q => q.fields.filter(field => !!field))
  79. )
  80. ) {
  81. this.fetchData();
  82. return;
  83. }
  84. // If the query names have changed, then update timeseries labels
  85. if (
  86. !loading &&
  87. !isEqual(widgetQueryNames, prevWidgetQueryNames) &&
  88. rawResults?.length === widget.queries.length
  89. ) {
  90. // eslint-disable-next-line react/no-did-update-set-state
  91. this.setState(prevState => {
  92. return {
  93. ...prevState,
  94. timeseriesResults: prevState.rawResults?.flatMap((rawResult, index) =>
  95. transformMetricsResponseToSeries(rawResult, widget.queries[index].name)
  96. ),
  97. };
  98. });
  99. }
  100. }
  101. componentWillUnmount() {
  102. this._isMounted = false;
  103. }
  104. private _isMounted: boolean = false;
  105. fetchTabularData(_queryFetchID: symbol) {
  106. this.setState({loading: false, tableResults: []});
  107. // TODO(dam): implement the rest
  108. }
  109. fetchTimeseriesData(queryFetchID: symbol) {
  110. const {selection, api, organization, widget} = this.props;
  111. this.setState({loading: false, timeseriesResults: [], rawResults: []});
  112. const {environments, projects, datetime} = selection;
  113. const {start, end, period} = datetime;
  114. const interval = getWidgetInterval(widget, {start, end, period});
  115. const promises = widget.queries.map(query => {
  116. const requestData = {
  117. field: query.fields,
  118. orgSlug: organization.slug,
  119. end,
  120. environment: environments,
  121. // groupBy: query.groupBy // TODO(dam): add backend groupBy support
  122. interval,
  123. limit: widget.displayType === DisplayType.TOP_N ? TOP_N : undefined,
  124. orderBy: query.orderby,
  125. project: projects,
  126. query: query.conditions,
  127. start,
  128. statsPeriod: period,
  129. };
  130. return doMetricsRequest(api, requestData);
  131. });
  132. let completed = 0;
  133. promises.forEach(async (promise, requestIndex) => {
  134. try {
  135. const rawResults = await promise;
  136. if (!this._isMounted) {
  137. return;
  138. }
  139. this.setState(prevState => {
  140. if (prevState.queryFetchID !== queryFetchID) {
  141. // invariant: a different request was initiated after this request
  142. return prevState;
  143. }
  144. const timeseriesResults = [...(prevState.timeseriesResults ?? [])];
  145. const transformedResult = transformMetricsResponseToSeries(
  146. rawResults,
  147. widget.queries[requestIndex].name
  148. );
  149. // When charting timeseriesData on echarts, color association to a timeseries result
  150. // is order sensitive, ie series at index i on the timeseries array will use color at
  151. // index i on the color array. This means that on multi series results, we need to make
  152. // sure that the order of series in our results do not change between fetches to avoid
  153. // coloring inconsistencies between renders.
  154. transformedResult.forEach((result, resultIndex) => {
  155. timeseriesResults[requestIndex * transformedResult.length + resultIndex] =
  156. result;
  157. });
  158. const rawResultsClone = cloneDeep(prevState.rawResults ?? []);
  159. rawResultsClone[requestIndex] = rawResults;
  160. return {
  161. ...prevState,
  162. timeseriesResults,
  163. rawResults: rawResultsClone,
  164. };
  165. });
  166. } catch (err) {
  167. const errorMessage = err?.responseJSON?.detail || t('An unknown error occurred.');
  168. this.setState({errorMessage});
  169. } finally {
  170. completed++;
  171. if (!this._isMounted) {
  172. return;
  173. }
  174. this.setState(prevState => {
  175. if (prevState.queryFetchID !== queryFetchID) {
  176. // invariant: a different request was initiated after this request
  177. return prevState;
  178. }
  179. return {
  180. ...prevState,
  181. loading: completed === promises.length ? false : true,
  182. };
  183. });
  184. }
  185. });
  186. }
  187. fetchData() {
  188. const {widget} = this.props;
  189. if (widget.displayType === DisplayType.WORLD_MAP) {
  190. this.setState({errorMessage: t('World Map is not supported by metrics.')});
  191. return;
  192. }
  193. const queryFetchID = Symbol('queryFetchID');
  194. this.setState({loading: true, errorMessage: undefined, queryFetchID});
  195. if (['table', 'big_number'].includes(widget.displayType)) {
  196. this.fetchTabularData(queryFetchID);
  197. } else {
  198. this.fetchTimeseriesData(queryFetchID);
  199. }
  200. }
  201. render() {
  202. const {children} = this.props;
  203. const {loading, timeseriesResults, tableResults, errorMessage} = this.state;
  204. return children({
  205. loading,
  206. timeseriesResults,
  207. tableResults,
  208. errorMessage,
  209. });
  210. }
  211. }
  212. export default MetricsWidgetQueries;