metricWidgetQueries.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import {Component} from 'react';
  2. import isEqual from 'lodash/isEqual';
  3. import omit from 'lodash/omit';
  4. import {Client} from 'sentry/api';
  5. import {isSelectionEqual} from 'sentry/components/organizations/pageFilters/utils';
  6. import {MetricsApiResponse, Organization, PageFilters} from 'sentry/types';
  7. import {Series} from 'sentry/types/echarts';
  8. import {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
  9. import {TOP_N} from 'sentry/utils/discover/types';
  10. import {MetricsConfig} from '../datasetConfig/metrics';
  11. import {DashboardFilters, DEFAULT_TABLE_LIMIT, DisplayType, Widget} from '../types';
  12. import GenericWidgetQueries, {
  13. GenericWidgetQueriesChildrenProps,
  14. GenericWidgetQueriesProps,
  15. } from './genericWidgetQueries';
  16. type Props = {
  17. api: Client;
  18. children: (props: GenericWidgetQueriesChildrenProps) => JSX.Element;
  19. organization: Organization;
  20. selection: PageFilters;
  21. widget: Widget;
  22. cursor?: string;
  23. dashboardFilters?: DashboardFilters;
  24. limit?: number;
  25. onDataFetched?: (results: {
  26. tableResults?: TableDataWithTitle[];
  27. timeseriesResults?: Series[];
  28. }) => void;
  29. };
  30. type State = {
  31. loading: boolean;
  32. errorMessage?: string;
  33. };
  34. class MetricWidgetQueries extends Component<Props, State> {
  35. state: State = {
  36. loading: true,
  37. errorMessage: undefined,
  38. };
  39. config = MetricsConfig;
  40. get limit() {
  41. const {limit} = this.props;
  42. switch (this.props.widget.displayType) {
  43. case DisplayType.TOP_N:
  44. return TOP_N;
  45. case DisplayType.TABLE:
  46. return limit ?? DEFAULT_TABLE_LIMIT;
  47. case DisplayType.BIG_NUMBER:
  48. return 1;
  49. default:
  50. return limit ?? 20; // TODO(dam): Can be changed to undefined once [INGEST-1079] is resolved
  51. }
  52. }
  53. customDidUpdateComparator = (
  54. prevProps: GenericWidgetQueriesProps<MetricsApiResponse, MetricsApiResponse>,
  55. nextProps: GenericWidgetQueriesProps<MetricsApiResponse, MetricsApiResponse>
  56. ) => {
  57. const {loading, limit, widget, cursor, organization, selection, dashboardFilters} =
  58. nextProps;
  59. const ignoredWidgetProps = [
  60. 'queries',
  61. 'title',
  62. 'id',
  63. 'layout',
  64. 'tempId',
  65. 'widgetType',
  66. ];
  67. const ignoredQueryProps = ['name', 'fields', 'aggregates', 'columns'];
  68. return (
  69. limit !== prevProps.limit ||
  70. organization.slug !== prevProps.organization.slug ||
  71. !isEqual(dashboardFilters, prevProps.dashboardFilters) ||
  72. !isSelectionEqual(selection, prevProps.selection) ||
  73. // If the widget changed (ignore unimportant fields, + queries as they are handled lower)
  74. !isEqual(
  75. omit(widget, ignoredWidgetProps),
  76. omit(prevProps.widget, ignoredWidgetProps)
  77. ) ||
  78. // If the queries changed (ignore unimportant name, + fields as they are handled lower)
  79. !isEqual(
  80. widget.queries.map(q => omit(q, ignoredQueryProps)),
  81. prevProps.widget.queries.map(q => omit(q, ignoredQueryProps))
  82. ) ||
  83. // If the fields changed (ignore falsy/empty fields -> they can happen after clicking on Add Overlay)
  84. !isEqual(
  85. widget.queries.flatMap(q => q.fields?.filter(field => !!field)),
  86. prevProps.widget.queries.flatMap(q => q.fields?.filter(field => !!field))
  87. ) ||
  88. !isEqual(
  89. widget.queries.flatMap(q => q.aggregates.filter(aggregate => !!aggregate)),
  90. prevProps.widget.queries.flatMap(q =>
  91. q.aggregates.filter(aggregate => !!aggregate)
  92. )
  93. ) ||
  94. !isEqual(
  95. widget.queries.flatMap(q => q.columns.filter(column => !!column)),
  96. prevProps.widget.queries.flatMap(q => q.columns.filter(column => !!column))
  97. ) ||
  98. loading !== prevProps.loading ||
  99. cursor !== prevProps.cursor
  100. );
  101. };
  102. afterFetchData = (data: MetricsApiResponse) => {
  103. const fields = this.props.widget.queries[0].aggregates;
  104. data.groups.forEach(group => {
  105. group.series = swapKeys(group.series, fields);
  106. group.totals = swapKeys(group.totals, fields);
  107. });
  108. };
  109. render() {
  110. const {
  111. api,
  112. children,
  113. organization,
  114. selection,
  115. widget,
  116. cursor,
  117. dashboardFilters,
  118. onDataFetched,
  119. } = this.props;
  120. const config = MetricsConfig;
  121. return (
  122. <GenericWidgetQueries<MetricsApiResponse, MetricsApiResponse>
  123. config={config}
  124. api={api}
  125. organization={organization}
  126. selection={selection}
  127. widget={widget}
  128. dashboardFilters={dashboardFilters}
  129. cursor={cursor}
  130. limit={this.limit}
  131. onDataFetched={onDataFetched}
  132. loading={undefined}
  133. customDidUpdateComparator={this.customDidUpdateComparator}
  134. afterFetchTableData={this.afterFetchData}
  135. afterFetchSeriesData={this.afterFetchData}
  136. >
  137. {({errorMessage, ...rest}) =>
  138. children({
  139. errorMessage: this.state.errorMessage ?? errorMessage,
  140. ...rest,
  141. })
  142. }
  143. </GenericWidgetQueries>
  144. );
  145. }
  146. }
  147. export default MetricWidgetQueries;
  148. const swapKeys = (obj: Record<string, unknown> | undefined, newKeys: string[]) => {
  149. if (!obj) {
  150. return {};
  151. }
  152. return Object.keys(obj).reduce((acc, key, index) => {
  153. acc[newKeys[index]] = obj[key];
  154. return acc;
  155. }, {});
  156. };