metricWidgetCard.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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 {MetricDisplayType} from 'sentry/utils/metrics/types';
  16. import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery';
  17. import {WidgetCardPanel, WidgetTitleRow} from 'sentry/views/dashboards/widgetCard';
  18. import {DashboardsMEPContext} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext';
  19. import {Toolbar} from 'sentry/views/dashboards/widgetCard/toolbar';
  20. import WidgetCardContextMenu from 'sentry/views/dashboards/widgetCard/widgetCardContextMenu';
  21. import {MetricChart} from 'sentry/views/ddm/chart';
  22. import {createChartPalette} from 'sentry/views/ddm/utils/metricsChartPalette';
  23. import {getChartTimeseries} from 'sentry/views/ddm/widget';
  24. import {LoadingScreen} from 'sentry/views/starfish/components/chart';
  25. import {convertToMetricWidget} from '../../../utils/metrics/dashboard';
  26. import {DASHBOARD_CHART_GROUP} from '../dashboard';
  27. import type {DashboardFilters, Widget} from '../types';
  28. type Props = {
  29. isEditingDashboard: boolean;
  30. location: Location;
  31. organization: Organization;
  32. router: InjectedRouter;
  33. selection: PageFilters;
  34. widget: Widget;
  35. dashboardFilters?: DashboardFilters;
  36. index?: string;
  37. isMobile?: boolean;
  38. onDelete?: () => void;
  39. onDuplicate?: () => void;
  40. onEdit?: (index: string) => void;
  41. renderErrorMessage?: (errorMessage?: string) => React.ReactNode;
  42. };
  43. export function MetricWidgetCard({
  44. organization,
  45. selection,
  46. widget,
  47. isEditingDashboard,
  48. onDelete,
  49. onDuplicate,
  50. location,
  51. router,
  52. dashboardFilters,
  53. renderErrorMessage,
  54. }: Props) {
  55. return (
  56. <DashboardsMEPContext.Provider
  57. value={{
  58. isMetricsData: undefined,
  59. setIsMetricsData: () => {},
  60. }}
  61. >
  62. <WidgetCardPanel isDragging={false}>
  63. <WidgetHeaderWrapper>
  64. <WidgetHeaderDescription>
  65. <WidgetTitleRow>
  66. <WidgetTitle>
  67. <TextOverflow>{widget.title}</TextOverflow>
  68. </WidgetTitle>
  69. </WidgetTitleRow>
  70. </WidgetHeaderDescription>
  71. <ContextMenuWrapper>
  72. {!isEditingDashboard && (
  73. <WidgetCardContextMenu
  74. organization={organization}
  75. widget={widget}
  76. selection={selection}
  77. showContextMenu
  78. isPreview={false}
  79. widgetLimitReached={false}
  80. onEdit={() => {
  81. router.push({
  82. pathname: `${location.pathname}${
  83. location.pathname.endsWith('/') ? '' : '/'
  84. }widget/${widget.id}/`,
  85. query: location.query,
  86. });
  87. }}
  88. router={router}
  89. location={location}
  90. onDelete={onDelete}
  91. onDuplicate={onDuplicate}
  92. />
  93. )}
  94. </ContextMenuWrapper>
  95. </WidgetHeaderWrapper>
  96. <MetricWidgetChartContainer
  97. selection={selection}
  98. widget={widget}
  99. dashboardFilters={dashboardFilters}
  100. renderErrorMessage={renderErrorMessage}
  101. />
  102. {isEditingDashboard && <Toolbar onDelete={onDelete} onDuplicate={onDuplicate} />}
  103. </WidgetCardPanel>
  104. </DashboardsMEPContext.Provider>
  105. );
  106. }
  107. type MetricWidgetChartContainerProps = {
  108. selection: PageFilters;
  109. widget: Widget;
  110. dashboardFilters?: DashboardFilters;
  111. renderErrorMessage?: (errorMessage?: string) => React.ReactNode;
  112. };
  113. export function MetricWidgetChartContainer({
  114. selection,
  115. widget,
  116. dashboardFilters,
  117. renderErrorMessage,
  118. }: MetricWidgetChartContainerProps) {
  119. const metricWidgetQueryParams = convertToMetricWidget(widget);
  120. const {projects, environments, datetime} = selection;
  121. const {mri, op, groupBy, displayType} = metricWidgetQueryParams;
  122. const chartQuery = useMemo(() => {
  123. return {
  124. mri,
  125. op,
  126. query: extendQuery(metricWidgetQueryParams.query, dashboardFilters),
  127. groupBy,
  128. };
  129. }, [mri, op, metricWidgetQueryParams.query, groupBy, dashboardFilters]);
  130. const {
  131. data: timeseriesData,
  132. isLoading,
  133. isError,
  134. error,
  135. } = useMetricsQuery(
  136. [chartQuery],
  137. {
  138. projects,
  139. environments,
  140. datetime,
  141. },
  142. {intervalLadder: displayType === MetricDisplayType.BAR ? 'bar' : 'dashboard'}
  143. );
  144. const chartRef = useRef<ReactEchartsRef>(null);
  145. const chartSeries = useMemo(() => {
  146. return timeseriesData
  147. ? getChartTimeseries(timeseriesData, [chartQuery], {
  148. getChartPalette: createChartPalette,
  149. })
  150. : [];
  151. }, [timeseriesData, chartQuery]);
  152. if (isError) {
  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={displayType}
  181. operation={op}
  182. widgetIndex={0}
  183. group={DASHBOARD_CHART_GROUP}
  184. />
  185. </TransitionChart>
  186. </MetricWidgetChartWrapper>
  187. );
  188. }
  189. function extendQuery(query = '', dashboardFilters?: DashboardFilters) {
  190. if (!dashboardFilters?.release?.length) {
  191. return query;
  192. }
  193. const releaseQuery = convertToQuery(dashboardFilters);
  194. return `${query} ${releaseQuery}`;
  195. }
  196. function convertToQuery(dashboardFilters: DashboardFilters) {
  197. const {release} = dashboardFilters;
  198. if (!release?.length) {
  199. return '';
  200. }
  201. if (release.length === 1) {
  202. return `release:${release[0]}`;
  203. }
  204. return `release:[${release.join(',')}]`;
  205. }
  206. const WidgetHeaderWrapper = styled('div')`
  207. min-height: 36px;
  208. width: 100%;
  209. display: flex;
  210. align-items: flex-start;
  211. justify-content: space-between;
  212. `;
  213. const ContextMenuWrapper = styled('div')`
  214. padding: ${space(2)} ${space(1)} 0 ${space(3)};
  215. `;
  216. const WidgetHeaderDescription = styled('div')`
  217. ${p => p.theme.overflowEllipsis};
  218. overflow-y: visible;
  219. `;
  220. const WidgetTitle = styled(HeaderTitle)`
  221. padding-left: ${space(3)};
  222. padding-top: ${space(2)};
  223. padding-right: ${space(1)};
  224. ${p => p.theme.overflowEllipsis};
  225. font-weight: normal;
  226. `;
  227. const MetricWidgetChartWrapper = styled('div')`
  228. height: 100%;
  229. width: 100%;
  230. padding: ${space(3)};
  231. padding-top: ${space(2)};
  232. `;