index.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import {useCallback, useMemo, useState} 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 TextOverflow from 'sentry/components/textOverflow';
  8. import {IconWarning} from 'sentry/icons';
  9. import {space} from 'sentry/styles/space';
  10. import type {MRI, Organization, PageFilters} from 'sentry/types';
  11. import {stringifyMetricWidget} from 'sentry/utils/metrics';
  12. import type {MetricWidgetQueryParams} from 'sentry/utils/metrics/types';
  13. import {WidgetCardPanel, WidgetTitleRow} from 'sentry/views/dashboards/widgetCard';
  14. import type {AugmentedEChartDataZoomHandler} from 'sentry/views/dashboards/widgetCard/chart';
  15. import {DashboardsMEPContext} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext';
  16. import {InlineEditor} from 'sentry/views/dashboards/widgetCard/metricWidgetCard/inlineEditor';
  17. import {Toolbar} from 'sentry/views/dashboards/widgetCard/toolbar';
  18. import WidgetCardContextMenu from 'sentry/views/dashboards/widgetCard/widgetCardContextMenu';
  19. import {MetricWidgetBody} from 'sentry/views/ddm/widget';
  20. import {
  21. convertToDashboardWidget,
  22. toMetricDisplayType,
  23. } from '../../../../utils/metrics/dashboard';
  24. import {parseField} from '../../../../utils/metrics/mri';
  25. import type {DashboardFilters, Widget} from '../../types';
  26. type Props = {
  27. isEditingDashboard: boolean;
  28. location: Location;
  29. organization: Organization;
  30. router: InjectedRouter;
  31. selection: PageFilters;
  32. widget: Widget;
  33. dashboardFilters?: DashboardFilters;
  34. index?: string;
  35. isEditingWidget?: boolean;
  36. isMobile?: boolean;
  37. onDelete?: () => void;
  38. onDuplicate?: () => void;
  39. onEdit?: (index: string) => void;
  40. onUpdate?: (widget: Widget | null) => void;
  41. onZoom?: AugmentedEChartDataZoomHandler;
  42. showSlider?: boolean;
  43. tableItemLimit?: number;
  44. windowWidth?: number;
  45. };
  46. export function MetricWidgetCard({
  47. organization,
  48. selection,
  49. widget,
  50. isEditingWidget,
  51. isEditingDashboard,
  52. onEdit,
  53. onUpdate,
  54. onDelete,
  55. onDuplicate,
  56. location,
  57. router,
  58. index,
  59. dashboardFilters,
  60. }: Props) {
  61. const [metricWidgetQueryParams, setMetricWidgetQueryParams] =
  62. useState<MetricWidgetQueryParams>(convertFromWidget(widget));
  63. const defaultTitle = useMemo(
  64. () => stringifyMetricWidget(metricWidgetQueryParams),
  65. [metricWidgetQueryParams]
  66. );
  67. const [title, setTitle] = useState<string>(widget.title ?? defaultTitle);
  68. const handleChange = useCallback(
  69. (data: Partial<MetricWidgetQueryParams>) => {
  70. setMetricWidgetQueryParams(curr => ({
  71. ...curr,
  72. ...data,
  73. }));
  74. },
  75. [setMetricWidgetQueryParams]
  76. );
  77. const handleSubmit = useCallback(() => {
  78. const convertedWidget = convertToDashboardWidget(
  79. {...selection, ...metricWidgetQueryParams},
  80. toMetricDisplayType(metricWidgetQueryParams.displayType)
  81. );
  82. const isCustomTitle = title !== '' && title !== defaultTitle;
  83. const updatedWidget = {
  84. ...widget,
  85. // If user renamed the widget, preserve that title, otherwise stringify the widget query params
  86. title: isCustomTitle ? title : defaultTitle,
  87. queries: convertedWidget.queries,
  88. displayType: convertedWidget.displayType,
  89. };
  90. onUpdate?.(updatedWidget);
  91. }, [title, defaultTitle, metricWidgetQueryParams, onUpdate, widget, selection]);
  92. const handleCancel = useCallback(() => {
  93. onUpdate?.(null);
  94. }, [onUpdate]);
  95. if (!metricWidgetQueryParams.mri) {
  96. return (
  97. <ErrorPanel height="200px">
  98. <IconWarning color="gray300" size="lg" />
  99. </ErrorPanel>
  100. );
  101. }
  102. return (
  103. <DashboardsMEPContext.Provider
  104. value={{
  105. isMetricsData: undefined,
  106. setIsMetricsData: () => {},
  107. }}
  108. >
  109. <WidgetCardPanel isDragging={false}>
  110. <WidgetHeaderWrapper>
  111. {isEditingWidget ? (
  112. <InlineEditor
  113. isEdit={!!isEditingWidget}
  114. displayType={metricWidgetQueryParams.displayType}
  115. metricsQuery={metricWidgetQueryParams}
  116. projects={selection.projects}
  117. powerUserMode={false}
  118. onChange={handleChange}
  119. onSubmit={handleSubmit}
  120. onCancel={handleCancel}
  121. onTitleChange={setTitle}
  122. title={title}
  123. />
  124. ) : (
  125. <WidgetHeaderDescription>
  126. <WidgetTitleRow>
  127. <WidgetTitle>
  128. <TextOverflow>{title}</TextOverflow>
  129. </WidgetTitle>
  130. </WidgetTitleRow>
  131. </WidgetHeaderDescription>
  132. )}
  133. <ContextMenuWrapper>
  134. {!isEditingDashboard && (
  135. <WidgetCardContextMenu
  136. organization={organization}
  137. widget={widget}
  138. selection={selection}
  139. showContextMenu
  140. isPreview={false}
  141. widgetLimitReached={false}
  142. onEdit={() => index && onEdit?.(index)}
  143. router={router}
  144. location={location}
  145. onDelete={onDelete}
  146. onDuplicate={onDuplicate}
  147. />
  148. )}
  149. </ContextMenuWrapper>
  150. </WidgetHeaderWrapper>
  151. <MetricWidgetChartWrapper>
  152. <MetricWidgetChartContainer
  153. selection={selection}
  154. widget={widget}
  155. editorParams={metricWidgetQueryParams}
  156. dashboardFilters={dashboardFilters}
  157. />
  158. </MetricWidgetChartWrapper>
  159. {isEditingDashboard && <Toolbar onDelete={onDelete} onDuplicate={onDuplicate} />}
  160. </WidgetCardPanel>
  161. </DashboardsMEPContext.Provider>
  162. );
  163. }
  164. type MetricWidgetChartContainerProps = {
  165. selection: PageFilters;
  166. widget: Widget;
  167. dashboardFilters?: DashboardFilters;
  168. editorParams?: Partial<MetricWidgetQueryParams>;
  169. };
  170. export function MetricWidgetChartContainer({
  171. selection,
  172. widget,
  173. editorParams = {},
  174. dashboardFilters,
  175. }: MetricWidgetChartContainerProps) {
  176. const metricWidgetQueryParams = {
  177. ...convertFromWidget(widget),
  178. ...editorParams,
  179. };
  180. return (
  181. <MetricWidgetBody
  182. widgetIndex={0}
  183. datetime={selection.datetime}
  184. projects={selection.projects}
  185. environments={selection.environments}
  186. mri={metricWidgetQueryParams.mri}
  187. op={metricWidgetQueryParams.op}
  188. query={extendQuery(metricWidgetQueryParams.query, dashboardFilters)}
  189. groupBy={metricWidgetQueryParams.groupBy}
  190. displayType={toMetricDisplayType(metricWidgetQueryParams.displayType)}
  191. />
  192. );
  193. }
  194. function extendQuery(query = '', dashboardFilters?: DashboardFilters) {
  195. if (!dashboardFilters?.release?.length) {
  196. return query;
  197. }
  198. const releaseQuery = convertToQuery(dashboardFilters);
  199. return `${query} ${releaseQuery}`;
  200. }
  201. function convertToQuery(dashboardFilters: DashboardFilters) {
  202. const {release} = dashboardFilters;
  203. if (!release?.length) {
  204. return '';
  205. }
  206. if (release.length === 1) {
  207. return `release:${release[0]}`;
  208. }
  209. return `release:[${release.join(',')}]`;
  210. }
  211. function convertFromWidget(widget: Widget): MetricWidgetQueryParams {
  212. const query = widget.queries[0];
  213. const parsed = parseField(query.aggregates[0]) || {mri: '' as MRI, op: ''};
  214. return {
  215. mri: parsed.mri,
  216. op: parsed.op,
  217. query: query.conditions,
  218. groupBy: query.columns,
  219. title: widget.title,
  220. displayType: toMetricDisplayType(widget.displayType),
  221. };
  222. }
  223. const WidgetHeaderWrapper = styled('div')`
  224. min-height: 36px;
  225. width: 100%;
  226. display: flex;
  227. align-items: flex-start;
  228. justify-content: space-between;
  229. `;
  230. const ContextMenuWrapper = styled('div')`
  231. padding: ${space(2)} ${space(1)} 0 ${space(3)};
  232. `;
  233. const WidgetHeaderDescription = styled('div')`
  234. ${p => p.theme.overflowEllipsis};
  235. overflow-y: visible;
  236. `;
  237. const WidgetTitle = styled(HeaderTitle)`
  238. padding-left: ${space(3)};
  239. padding-top: ${space(2)};
  240. padding-right: ${space(1)};
  241. ${p => p.theme.overflowEllipsis};
  242. font-weight: normal;
  243. `;
  244. const MetricWidgetChartWrapper = styled('div')`
  245. height: 100%;
  246. width: 100%;
  247. padding: ${space(2)};
  248. `;