metricWidgetViewerModal.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import {Fragment, useCallback, useMemo, useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  4. import {Button, LinkButton} from 'sentry/components/button';
  5. import ButtonBar from 'sentry/components/buttonBar';
  6. import {
  7. MetricWidgetTitle,
  8. type MetricWidgetTitleState,
  9. } from 'sentry/components/modals/metricWidgetViewerModal/header';
  10. import {Queries} from 'sentry/components/modals/metricWidgetViewerModal/queries';
  11. import {MetricVisualization} from 'sentry/components/modals/metricWidgetViewerModal/visualization';
  12. import type {WidgetViewerModalOptions} from 'sentry/components/modals/widgetViewerModal';
  13. import {t} from 'sentry/locale';
  14. import type {Organization} from 'sentry/types';
  15. import {getDdmUrl} from 'sentry/utils/metrics';
  16. import {toDisplayType} from 'sentry/utils/metrics/dashboard';
  17. import {MetricQueryType} from 'sentry/utils/metrics/types';
  18. import usePageFilters from 'sentry/utils/usePageFilters';
  19. import type {
  20. DashboardMetricsEquation,
  21. DashboardMetricsQuery,
  22. Order,
  23. } from 'sentry/views/dashboards/metrics/types';
  24. import {
  25. expressionsToApiQueries,
  26. expressionsToWidget,
  27. filterEquationsByDisplayType,
  28. filterQueriesByDisplayType,
  29. getMetricEquations,
  30. getMetricQueries,
  31. getMetricWidgetTitle,
  32. useGenerateExpressionId,
  33. } from 'sentry/views/dashboards/metrics/utils';
  34. import {MetricDetails} from 'sentry/views/ddm/widgetDetails';
  35. import {OrganizationContext} from 'sentry/views/organizationContext';
  36. interface Props extends ModalRenderProps, WidgetViewerModalOptions {
  37. organization: Organization;
  38. }
  39. function MetricWidgetViewerModal({
  40. organization,
  41. widget,
  42. Footer,
  43. Body,
  44. Header,
  45. closeModal,
  46. onMetricWidgetEdit,
  47. dashboardFilters,
  48. }: Props) {
  49. const {selection} = usePageFilters();
  50. const [displayType, setDisplayType] = useState(widget.displayType);
  51. const [metricQueries, setMetricQueries] = useState<DashboardMetricsQuery[]>(() =>
  52. getMetricQueries(widget, dashboardFilters)
  53. );
  54. const [metricEquations, setMetricEquations] = useState<DashboardMetricsEquation[]>(() =>
  55. getMetricEquations(widget)
  56. );
  57. const filteredQueries = useMemo(
  58. () => filterQueriesByDisplayType(metricQueries, displayType),
  59. [metricQueries, displayType]
  60. );
  61. const filteredEquations = useMemo(
  62. () =>
  63. filterEquationsByDisplayType(metricEquations, displayType).filter(
  64. equation => equation.formula !== ''
  65. ),
  66. [metricEquations, displayType]
  67. );
  68. const generateQueryId = useGenerateExpressionId(metricQueries);
  69. const generateEquationId = useGenerateExpressionId(metricEquations);
  70. const apiQueries = useMemo(
  71. () => expressionsToApiQueries([...filteredQueries, ...filteredEquations]),
  72. [filteredQueries, filteredEquations]
  73. );
  74. const widgetMQL = useMemo(
  75. () => getMetricWidgetTitle([...filteredQueries, ...filteredEquations]),
  76. [filteredQueries, filteredEquations]
  77. );
  78. const [title, setTitle] = useState<MetricWidgetTitleState>({
  79. stored: widget.title,
  80. edited: widget.title,
  81. isEditing: false,
  82. });
  83. const handleTitleChange = useCallback(
  84. (patch: Partial<MetricWidgetTitleState>) => {
  85. setTitle(curr => ({...curr, ...patch}));
  86. },
  87. [setTitle]
  88. );
  89. const handleQueryChange = useCallback(
  90. (data: Partial<DashboardMetricsQuery>, index: number) => {
  91. setMetricQueries(curr => {
  92. const updated = [...curr];
  93. updated[index] = {...updated[index], ...data} as DashboardMetricsQuery;
  94. return updated;
  95. });
  96. },
  97. [setMetricQueries]
  98. );
  99. const handleEquationChange = useCallback(
  100. (data: Partial<DashboardMetricsEquation>, index: number) => {
  101. setMetricEquations(curr => {
  102. const updated = [...curr];
  103. updated[index] = {...updated[index], ...data} as DashboardMetricsEquation;
  104. return updated;
  105. });
  106. },
  107. [setMetricEquations]
  108. );
  109. const handleOrderChange = useCallback(
  110. ({id, order}: {id: number; order: Order}) => {
  111. const queryIdx = filteredQueries.findIndex(query => query.id === id);
  112. if (queryIdx > -1) {
  113. setMetricQueries(curr => {
  114. return curr.map((query, i) => {
  115. const orderBy = i === queryIdx ? order : undefined;
  116. return {...query, orderBy};
  117. });
  118. });
  119. return;
  120. }
  121. const equationIdx = filteredEquations.findIndex(equation => equation.id === id);
  122. if (equationIdx > -1) {
  123. setMetricEquations(curr => {
  124. return curr.map((equation, i) => {
  125. const orderBy = i === equationIdx ? order : undefined;
  126. return {...equation, orderBy};
  127. });
  128. });
  129. }
  130. },
  131. [filteredEquations, filteredQueries]
  132. );
  133. const addQuery = useCallback(() => {
  134. setMetricQueries(curr => {
  135. return [
  136. ...curr,
  137. {
  138. ...metricQueries[metricQueries.length - 1],
  139. id: generateQueryId(),
  140. },
  141. ];
  142. });
  143. }, [generateQueryId, metricQueries]);
  144. const addEquation = useCallback(() => {
  145. setMetricEquations(curr => {
  146. return [
  147. ...curr,
  148. {
  149. formula: '',
  150. name: '',
  151. id: generateEquationId(),
  152. type: MetricQueryType.FORMULA,
  153. },
  154. ];
  155. });
  156. }, [generateEquationId]);
  157. const removeEquation = useCallback(
  158. (index: number) => {
  159. setMetricEquations(curr => {
  160. const updated = [...curr];
  161. updated.splice(index, 1);
  162. return updated;
  163. });
  164. },
  165. [setMetricEquations]
  166. );
  167. const removeQuery = useCallback(
  168. (index: number) => {
  169. setMetricQueries(curr => {
  170. const updated = [...curr];
  171. updated.splice(index, 1);
  172. return updated;
  173. });
  174. },
  175. [setMetricQueries]
  176. );
  177. const handleSubmit = useCallback(() => {
  178. const convertedWidget = expressionsToWidget(
  179. [...filteredQueries, ...filteredEquations],
  180. title.edited,
  181. toDisplayType(displayType)
  182. );
  183. onMetricWidgetEdit?.(convertedWidget);
  184. closeModal();
  185. }, [
  186. filteredQueries,
  187. filteredEquations,
  188. title.edited,
  189. displayType,
  190. onMetricWidgetEdit,
  191. closeModal,
  192. ]);
  193. return (
  194. <Fragment>
  195. <OrganizationContext.Provider value={organization}>
  196. <Header closeButton>
  197. <MetricWidgetTitle
  198. title={title}
  199. onTitleChange={handleTitleChange}
  200. placeholder={widgetMQL}
  201. description={widget.description}
  202. />
  203. </Header>
  204. <Body>
  205. <Queries
  206. displayType={displayType}
  207. metricQueries={metricQueries}
  208. metricEquations={metricEquations}
  209. onQueryChange={handleQueryChange}
  210. onEquationChange={handleEquationChange}
  211. addEquation={addEquation}
  212. addQuery={addQuery}
  213. removeEquation={removeEquation}
  214. removeQuery={removeQuery}
  215. />
  216. <MetricVisualization
  217. queries={apiQueries}
  218. displayType={displayType}
  219. onDisplayTypeChange={setDisplayType}
  220. onOrderChange={handleOrderChange}
  221. />
  222. <MetricDetails mri={metricQueries[0].mri} query={metricQueries[0].query} />
  223. </Body>
  224. <Footer>
  225. <ButtonBar gap={1}>
  226. <LinkButton
  227. to={getDdmUrl(organization.slug, {
  228. widgets: [...metricQueries, ...metricEquations],
  229. ...selection.datetime,
  230. project: selection.projects,
  231. environment: selection.environments,
  232. })}
  233. >
  234. {t('Open in Metrics')}
  235. </LinkButton>
  236. <Button onClick={closeModal}>{t('Close')}</Button>
  237. <Button priority="primary" onClick={handleSubmit}>
  238. {t('Save changes')}
  239. </Button>
  240. </ButtonBar>
  241. </Footer>
  242. </OrganizationContext.Provider>
  243. </Fragment>
  244. );
  245. }
  246. export default MetricWidgetViewerModal;
  247. export const modalCss = css`
  248. width: 100%;
  249. max-width: 1200px;
  250. `;