widgetCard.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import {Fragment, useMemo, useRef} from 'react';
  2. import type {InjectedRouter} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import type {Location} from 'history';
  5. import ErrorPanel from 'sentry/components/charts/errorPanel';
  6. import {HeaderTitle} from 'sentry/components/charts/styles';
  7. import TransitionChart from 'sentry/components/charts/transitionChart';
  8. import EmptyMessage from 'sentry/components/emptyMessage';
  9. import TextOverflow from 'sentry/components/textOverflow';
  10. import {IconSearch, IconWarning} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import type {Organization, PageFilters} from 'sentry/types';
  14. import type {ReactEchartsRef} from 'sentry/types/echarts';
  15. import {getWidgetTitle} from 'sentry/utils/metrics';
  16. import {
  17. type MetricsQueryApiRequestQuery,
  18. useMetricsQuery,
  19. } from 'sentry/utils/metrics/useMetricsQuery';
  20. import {DASHBOARD_CHART_GROUP} from 'sentry/views/dashboards/dashboard';
  21. import {
  22. getMetricQueries,
  23. toMetricDisplayType,
  24. } from 'sentry/views/dashboards/metrics/utils';
  25. import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types';
  26. import {DisplayType} from 'sentry/views/dashboards/types';
  27. import {WidgetCardPanel, WidgetTitleRow} from 'sentry/views/dashboards/widgetCard';
  28. import {DashboardsMEPContext} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext';
  29. import {Toolbar} from 'sentry/views/dashboards/widgetCard/toolbar';
  30. import WidgetCardContextMenu from 'sentry/views/dashboards/widgetCard/widgetCardContextMenu';
  31. import {MetricChart} from 'sentry/views/ddm/chart/chart';
  32. import {createChartPalette} from 'sentry/views/ddm/utils/metricsChartPalette';
  33. import {getChartTimeseries} from 'sentry/views/ddm/widget';
  34. import {LoadingScreen} from 'sentry/views/starfish/components/chart';
  35. type Props = {
  36. isEditingDashboard: boolean;
  37. location: Location;
  38. organization: Organization;
  39. router: InjectedRouter;
  40. selection: PageFilters;
  41. widget: Widget;
  42. dashboardFilters?: DashboardFilters;
  43. index?: string;
  44. isMobile?: boolean;
  45. onDelete?: () => void;
  46. onDuplicate?: () => void;
  47. onEdit?: (index: string) => void;
  48. renderErrorMessage?: (errorMessage?: string) => React.ReactNode;
  49. showContextMenu?: boolean;
  50. };
  51. export function MetricWidgetCard({
  52. organization,
  53. selection,
  54. widget,
  55. isEditingDashboard,
  56. onDelete,
  57. onDuplicate,
  58. location,
  59. router,
  60. dashboardFilters,
  61. renderErrorMessage,
  62. showContextMenu = true,
  63. }: Props) {
  64. const metricQueries = useMemo(
  65. () => getMetricQueries(widget, dashboardFilters),
  66. [widget, dashboardFilters]
  67. );
  68. const widgetMQL = useMemo(() => getWidgetTitle(metricQueries), [metricQueries]);
  69. return (
  70. <DashboardsMEPContext.Provider
  71. value={{
  72. isMetricsData: undefined,
  73. setIsMetricsData: () => {},
  74. }}
  75. >
  76. <WidgetCardPanel isDragging={false}>
  77. <WidgetHeaderWrapper>
  78. <WidgetHeaderDescription>
  79. <WidgetTitleRow>
  80. <WidgetTitle>
  81. <TextOverflow>{widget.title || widgetMQL}</TextOverflow>
  82. </WidgetTitle>
  83. </WidgetTitleRow>
  84. </WidgetHeaderDescription>
  85. <ContextMenuWrapper>
  86. {showContextMenu && !isEditingDashboard && (
  87. <WidgetCardContextMenu
  88. organization={organization}
  89. widget={widget}
  90. selection={selection}
  91. showContextMenu
  92. isPreview={false}
  93. widgetLimitReached={false}
  94. onEdit={() => {
  95. router.push({
  96. pathname: `${location.pathname}${
  97. location.pathname.endsWith('/') ? '' : '/'
  98. }widget/${widget.id}/`,
  99. query: location.query,
  100. });
  101. }}
  102. router={router}
  103. location={location}
  104. onDelete={onDelete}
  105. onDuplicate={onDuplicate}
  106. />
  107. )}
  108. </ContextMenuWrapper>
  109. </WidgetHeaderWrapper>
  110. <MetricWidgetChartContainer
  111. metricQueries={metricQueries}
  112. selection={selection}
  113. renderErrorMessage={renderErrorMessage}
  114. chartHeight={!showContextMenu ? 200 : undefined}
  115. displayType={widget.displayType}
  116. />
  117. {isEditingDashboard && <Toolbar onDelete={onDelete} onDuplicate={onDuplicate} />}
  118. </WidgetCardPanel>
  119. </DashboardsMEPContext.Provider>
  120. );
  121. }
  122. type MetricWidgetChartContainerProps = {
  123. displayType: DisplayType;
  124. metricQueries: MetricsQueryApiRequestQuery[];
  125. selection: PageFilters;
  126. chartHeight?: number;
  127. renderErrorMessage?: (errorMessage?: string) => React.ReactNode;
  128. };
  129. export function MetricWidgetChartContainer({
  130. selection,
  131. renderErrorMessage,
  132. metricQueries,
  133. chartHeight,
  134. displayType,
  135. }: MetricWidgetChartContainerProps) {
  136. const {
  137. data: timeseriesData,
  138. isLoading,
  139. isError,
  140. error,
  141. } = useMetricsQuery(metricQueries, selection, {
  142. intervalLadder: displayType === DisplayType.BAR ? 'bar' : 'dashboard',
  143. });
  144. const chartRef = useRef<ReactEchartsRef>(null);
  145. const chartSeries = useMemo(() => {
  146. return timeseriesData
  147. ? getChartTimeseries(timeseriesData, metricQueries, {
  148. getChartPalette: createChartPalette,
  149. })
  150. : [];
  151. }, [timeseriesData, metricQueries]);
  152. if (isError && !timeseriesData) {
  153. const errorMessage =
  154. error?.responseJSON?.detail?.toString() || t('Error while fetching metrics data');
  155. return (
  156. <Fragment>
  157. {renderErrorMessage?.(errorMessage)}
  158. <ErrorPanel>
  159. <IconWarning color="gray500" size="lg" />
  160. </ErrorPanel>
  161. </Fragment>
  162. );
  163. }
  164. if (timeseriesData?.data.length === 0) {
  165. return (
  166. <EmptyMessage
  167. icon={<IconSearch size="xxl" />}
  168. title={t('No results')}
  169. description={t('No results found for the given query')}
  170. />
  171. );
  172. }
  173. return (
  174. <MetricWidgetChartWrapper>
  175. <TransitionChart loading={isLoading} reloading={isLoading}>
  176. <LoadingScreen loading={isLoading} />
  177. <MetricChart
  178. ref={chartRef}
  179. series={chartSeries}
  180. displayType={toMetricDisplayType(displayType)}
  181. widgetIndex={0}
  182. group={DASHBOARD_CHART_GROUP}
  183. height={chartHeight}
  184. />
  185. </TransitionChart>
  186. </MetricWidgetChartWrapper>
  187. );
  188. }
  189. const WidgetHeaderWrapper = styled('div')`
  190. min-height: 36px;
  191. width: 100%;
  192. display: flex;
  193. align-items: flex-start;
  194. justify-content: space-between;
  195. `;
  196. const ContextMenuWrapper = styled('div')`
  197. padding: ${space(2)} ${space(1)} 0 ${space(3)};
  198. `;
  199. const WidgetHeaderDescription = styled('div')`
  200. ${p => p.theme.overflowEllipsis};
  201. overflow-y: visible;
  202. `;
  203. const WidgetTitle = styled(HeaderTitle)`
  204. padding-left: ${space(3)};
  205. padding-top: ${space(2)};
  206. padding-right: ${space(1)};
  207. ${p => p.theme.overflowEllipsis};
  208. font-weight: normal;
  209. `;
  210. const MetricWidgetChartWrapper = styled('div')`
  211. height: 100%;
  212. width: 100%;
  213. padding: ${space(3)};
  214. padding-top: ${space(2)};
  215. `;