singleFieldAreaWidget.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import pick from 'lodash/pick';
  4. import _EventsRequest from 'sentry/components/charts/eventsRequest';
  5. import {getInterval, getPreviousSeriesName} from 'sentry/components/charts/utils';
  6. import {t} from 'sentry/locale';
  7. import {axisLabelFormatter} from 'sentry/utils/discover/charts';
  8. import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
  9. import {aggregateOutputType} from 'sentry/utils/discover/fields';
  10. import {
  11. QueryBatchNode,
  12. Transform,
  13. } from 'sentry/utils/performance/contexts/genericQueryBatcher';
  14. import {useMEPSettingContext} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  15. import {usePageError} from 'sentry/utils/performance/contexts/pageError';
  16. import {useLocation} from 'sentry/utils/useLocation';
  17. import withApi from 'sentry/utils/withApi';
  18. import DurationChart from 'sentry/views/performance/charts/chart';
  19. import {GenericPerformanceWidget} from '../components/performanceWidget';
  20. import {transformDiscoverToSingleValue} from '../transforms/transformDiscoverToSingleValue';
  21. import {transformEventsRequestToArea} from '../transforms/transformEventsToArea';
  22. import {PerformanceWidgetProps, QueryDefinition, WidgetDataResult} from '../types';
  23. import {eventsRequestQueryProps, getMEPQueryParams} from '../utils';
  24. type DataType = {
  25. chart: WidgetDataResult & ReturnType<typeof transformEventsRequestToArea>;
  26. overall: WidgetDataResult & ReturnType<typeof transformDiscoverToSingleValue>;
  27. };
  28. export function SingleFieldAreaWidget(props: PerformanceWidgetProps) {
  29. const location = useLocation();
  30. const {ContainerActions, InteractiveTitle} = props;
  31. const globalSelection = props.eventView.getPageFilters();
  32. const pageError = usePageError();
  33. const mepSetting = useMEPSettingContext();
  34. if (props.fields.length !== 1) {
  35. throw new Error(`Single field area can only accept a single field (${props.fields})`);
  36. }
  37. const field = props.fields[0];
  38. const chartQuery = useMemo<QueryDefinition<DataType, WidgetDataResult>>(
  39. () => ({
  40. fields: props.fields[0],
  41. component: provided => (
  42. <QueryBatchNode batchProperty="yAxis" transform={unmergeIntoIndividualResults}>
  43. {({queryBatching}) => (
  44. <EventsRequest
  45. {...pick(provided, eventsRequestQueryProps)}
  46. limit={1}
  47. queryBatching={queryBatching}
  48. includePrevious
  49. includeTransformedData
  50. partial
  51. currentSeriesNames={[field]}
  52. previousSeriesNames={[getPreviousSeriesName(field)]}
  53. query={provided.eventView.getQueryWithAdditionalConditions()}
  54. interval={getInterval(
  55. {
  56. start: provided.start,
  57. end: provided.end,
  58. period: provided.period,
  59. },
  60. 'medium'
  61. )}
  62. hideError
  63. onError={pageError.setPageError}
  64. queryExtras={getMEPQueryParams(mepSetting)}
  65. />
  66. )}
  67. </QueryBatchNode>
  68. ),
  69. transform: transformEventsRequestToArea,
  70. }),
  71. // eslint-disable-next-line react-hooks/exhaustive-deps
  72. [props.chartSetting, mepSetting.memoizationKey]
  73. );
  74. const overallQuery = useMemo<QueryDefinition<DataType, WidgetDataResult>>(
  75. () => ({
  76. fields: field,
  77. component: provided => {
  78. const eventView = provided.eventView.clone();
  79. eventView.sorts = [];
  80. eventView.fields = props.fields.map(fieldName => ({field: fieldName}));
  81. return (
  82. <QueryBatchNode batchProperty="field">
  83. {({queryBatching}) => (
  84. <DiscoverQuery
  85. {...provided}
  86. queryBatching={queryBatching}
  87. eventView={eventView}
  88. location={location}
  89. queryExtras={getMEPQueryParams(mepSetting)}
  90. />
  91. )}
  92. </QueryBatchNode>
  93. );
  94. },
  95. transform: transformDiscoverToSingleValue,
  96. }),
  97. // eslint-disable-next-line react-hooks/exhaustive-deps
  98. [props.chartSetting, mepSetting.memoizationKey]
  99. );
  100. const Queries = {
  101. chart: chartQuery,
  102. overall: overallQuery,
  103. };
  104. return (
  105. <GenericPerformanceWidget<DataType>
  106. {...props}
  107. location={location}
  108. Subtitle={() => (
  109. <Subtitle>
  110. {globalSelection.datetime.period
  111. ? t('Compared to last %s ', globalSelection.datetime.period)
  112. : t('Compared to the last period')}
  113. </Subtitle>
  114. )}
  115. InteractiveTitle={
  116. InteractiveTitle
  117. ? provided => <InteractiveTitle {...provided.widgetData.chart} />
  118. : null
  119. }
  120. HeaderActions={provided => (
  121. <Fragment>
  122. {provided.widgetData?.overall?.hasData ? (
  123. <Fragment>
  124. {props.fields.map(fieldName => {
  125. const value = provided.widgetData?.overall?.[fieldName];
  126. if (!value) {
  127. return null;
  128. }
  129. return (
  130. <HighlightNumber key={fieldName} color={props.chartColor}>
  131. {axisLabelFormatter(value, aggregateOutputType(fieldName))}
  132. </HighlightNumber>
  133. );
  134. })}
  135. </Fragment>
  136. ) : null}
  137. {ContainerActions && <ContainerActions {...provided.widgetData.chart} />}
  138. </Fragment>
  139. )}
  140. Queries={Queries}
  141. Visualizations={[
  142. {
  143. component: provided => (
  144. <DurationChart
  145. {...provided.widgetData.chart}
  146. {...provided}
  147. disableMultiAxis
  148. disableXAxis
  149. definedAxisTicks={4}
  150. chartColors={props.chartColor ? [props.chartColor] : undefined}
  151. />
  152. ),
  153. height: props.chartHeight,
  154. },
  155. ]}
  156. />
  157. );
  158. }
  159. const EventsRequest = withApi(_EventsRequest);
  160. export const Subtitle = styled('span')`
  161. color: ${p => p.theme.gray300};
  162. font-size: ${p => p.theme.fontSizeMedium};
  163. `;
  164. export const HighlightNumber = styled('div')<{color?: string}>`
  165. color: ${p => p.color};
  166. font-size: ${p => p.theme.fontSizeExtraLarge};
  167. `;
  168. const unmergeIntoIndividualResults: Transform = (response, queryDefinition) => {
  169. const propertyName = Array.isArray(
  170. queryDefinition.requestQueryObject.query[queryDefinition.batchProperty]
  171. )
  172. ? queryDefinition.requestQueryObject.query[queryDefinition.batchProperty][0]
  173. : queryDefinition.requestQueryObject.query[queryDefinition.batchProperty];
  174. return response[propertyName];
  175. };