performanceWidget.tsx 7.2 KB

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