performanceWidget.tsx 6.5 KB

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