performanceWidget.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import {Fragment, useCallback, useRef, useState} from 'react';
  2. // eslint-disable-next-line no-restricted-imports
  3. import {withRouter} from 'react-router';
  4. import styled from '@emotion/styled';
  5. import ErrorPanel from 'sentry/components/charts/errorPanel';
  6. import Placeholder from 'sentry/components/placeholder';
  7. import {IconWarning} from 'sentry/icons/iconWarning';
  8. import space from 'sentry/styles/space';
  9. import {Organization} from 'sentry/types';
  10. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  11. import getDynamicText from 'sentry/utils/getDynamicText';
  12. import {MEPDataProvider} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
  13. import useApi from 'sentry/utils/useApi';
  14. import getPerformanceWidgetContainer from 'sentry/views/performance/landing/widgets/components/performanceWidgetContainer';
  15. import {
  16. GenericPerformanceWidgetProps,
  17. WidgetDataConstraint,
  18. WidgetDataProps,
  19. WidgetDataResult,
  20. WidgetPropUnion,
  21. } from '../types';
  22. import {PerformanceWidgetSetting} from '../widgetDefinitions';
  23. import {DataStateSwitch} from './dataStateSwitch';
  24. import {QueryHandler} from './queryHandler';
  25. import {WidgetHeader} from './widgetHeader';
  26. // Generic performance widget for type T, where T defines all the data contained in the widget.
  27. export function GenericPerformanceWidget<T extends WidgetDataConstraint>(
  28. props: WidgetPropUnion<T>
  29. ) {
  30. // Use object keyed to chart setting so switching between charts of a similar type doesn't retain data with query components still having inflight requests.
  31. const [allWidgetData, setWidgetData] = useState<{[chartSetting: string]: T}>({});
  32. const widgetData = allWidgetData[props.chartSetting] ?? {};
  33. const widgetDataRef = useRef(widgetData);
  34. const setWidgetDataForKey = useCallback(
  35. (dataKey: string, result?: WidgetDataResult) => {
  36. const _widgetData = widgetDataRef.current;
  37. const newWidgetData = {..._widgetData, [dataKey]: result};
  38. widgetDataRef.current = newWidgetData;
  39. setWidgetData({[props.chartSetting]: newWidgetData});
  40. },
  41. // eslint-disable-next-line react-hooks/exhaustive-deps
  42. [allWidgetData, setWidgetData]
  43. );
  44. const removeWidgetDataForKey = useCallback(
  45. (dataKey: string) => {
  46. const _widgetData = widgetDataRef.current;
  47. const newWidgetData = {..._widgetData};
  48. delete newWidgetData[dataKey];
  49. widgetDataRef.current = newWidgetData;
  50. setWidgetData({[props.chartSetting]: newWidgetData});
  51. },
  52. // eslint-disable-next-line react-hooks/exhaustive-deps
  53. [allWidgetData, setWidgetData]
  54. );
  55. const widgetProps = {widgetData, setWidgetDataForKey, removeWidgetDataForKey};
  56. const queries = Object.entries(props.Queries).map(([key, definition]) => ({
  57. ...definition,
  58. queryKey: key,
  59. }));
  60. const api = useApi();
  61. const totalHeight = props.Visualizations.reduce((acc, curr) => acc + curr.height, 0);
  62. return (
  63. <Fragment>
  64. <MEPDataProvider chartSetting={props.chartSetting}>
  65. <QueryHandler
  66. eventView={props.eventView}
  67. widgetData={widgetData}
  68. setWidgetDataForKey={setWidgetDataForKey}
  69. removeWidgetDataForKey={removeWidgetDataForKey}
  70. queryProps={props}
  71. queries={queries}
  72. api={api}
  73. />
  74. <_DataDisplay<T> {...props} {...widgetProps} totalHeight={totalHeight} />
  75. </MEPDataProvider>
  76. </Fragment>
  77. );
  78. }
  79. function trackDataComponentClicks(
  80. chartSetting: PerformanceWidgetSetting,
  81. organization: Organization
  82. ) {
  83. trackAdvancedAnalyticsEvent('performance_views.landingv3.widget.interaction', {
  84. organization,
  85. widget_type: chartSetting,
  86. });
  87. }
  88. function _DataDisplay<T extends WidgetDataConstraint>(
  89. props: GenericPerformanceWidgetProps<T> & WidgetDataProps<T> & {totalHeight: number}
  90. ) {
  91. const {Visualizations, chartHeight, totalHeight, containerType, EmptyComponent} = props;
  92. const Container = getPerformanceWidgetContainer({
  93. containerType,
  94. });
  95. const numberKeys = Object.keys(props.Queries).length;
  96. const missingDataKeys = Object.values(props.widgetData).length !== numberKeys;
  97. const hasData =
  98. !missingDataKeys && Object.values(props.widgetData).every(d => !d || d.hasData);
  99. const isLoading = Object.values(props.widgetData).some(d => !d || d.isLoading);
  100. const isErrored =
  101. !missingDataKeys && Object.values(props.widgetData).some(d => d && d.isErrored);
  102. return (
  103. <Container data-test-id="performance-widget-container">
  104. <ContentContainer>
  105. <WidgetHeader<T> {...props} />
  106. </ContentContainer>
  107. <DataStateSwitch
  108. isLoading={isLoading}
  109. isErrored={isErrored}
  110. hasData={hasData}
  111. errorComponent={<DefaultErrorComponent height={totalHeight} />}
  112. dataComponents={Visualizations.map((Visualization, index) => (
  113. <ContentContainer
  114. key={index}
  115. noPadding={Visualization.noPadding}
  116. bottomPadding={Visualization.bottomPadding}
  117. data-test-id="widget-state-has-data"
  118. onClick={() =>
  119. trackDataComponentClicks(props.chartSetting, props.organization)
  120. }
  121. >
  122. {getDynamicText({
  123. value: (
  124. <Visualization.component
  125. grid={defaultGrid}
  126. queryFields={Visualization.fields}
  127. widgetData={props.widgetData}
  128. height={chartHeight}
  129. />
  130. ),
  131. fixed: <Placeholder height={`${chartHeight}px`} />,
  132. })}
  133. </ContentContainer>
  134. ))}
  135. loadingComponent={<PerformanceWidgetPlaceholder height={`${totalHeight}px`} />}
  136. emptyComponent={
  137. EmptyComponent ? (
  138. <EmptyComponent />
  139. ) : (
  140. <PerformanceWidgetPlaceholder height={`${totalHeight}px`} />
  141. )
  142. }
  143. />
  144. </Container>
  145. );
  146. }
  147. export const DataDisplay = withRouter(_DataDisplay);
  148. const DefaultErrorComponent = (props: {height: number}) => {
  149. return (
  150. <ErrorPanel data-test-id="widget-state-is-errored" height={`${props.height}px`}>
  151. <IconWarning color="gray300" size="lg" />
  152. </ErrorPanel>
  153. );
  154. };
  155. const defaultGrid = {
  156. left: space(0),
  157. right: space(0),
  158. top: space(2),
  159. bottom: space(1),
  160. };
  161. const ContentContainer = styled('div')<{bottomPadding?: boolean; noPadding?: boolean}>`
  162. padding-left: ${p => (p.noPadding ? space(0) : space(2))};
  163. padding-right: ${p => (p.noPadding ? space(0) : space(2))};
  164. padding-bottom: ${p => (p.bottomPadding ? space(1) : space(0))};
  165. `;
  166. const PerformanceWidgetPlaceholder = styled(Placeholder)`
  167. border-color: transparent;
  168. border-bottom-right-radius: inherit;
  169. border-bottom-left-radius: inherit;
  170. `;
  171. GenericPerformanceWidget.defaultProps = {
  172. containerType: 'panel',
  173. chartHeight: 200,
  174. };