performanceWidget.tsx 7.0 KB

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