index.tsx 7.0 KB

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