index.tsx 7.0 KB

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