index.tsx 7.3 KB

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