context.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import {
  2. createContext,
  3. useCallback,
  4. useContext,
  5. useEffect,
  6. useMemo,
  7. useState,
  8. } from 'react';
  9. import * as Sentry from '@sentry/react';
  10. import {MRI} from 'sentry/types';
  11. import {
  12. defaultMetricDisplayType,
  13. getAbsoluteDateTimeRange,
  14. MetricDisplayType,
  15. MetricWidgetQueryParams,
  16. useInstantRef,
  17. useUpdateQuery,
  18. } from 'sentry/utils/metrics';
  19. import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
  20. import {decodeList} from 'sentry/utils/queryString';
  21. import usePageFilters from 'sentry/utils/usePageFilters';
  22. import useRouter from 'sentry/utils/useRouter';
  23. import {FocusArea} from 'sentry/views/ddm/chartBrush';
  24. import {DEFAULT_SORT_STATE} from 'sentry/views/ddm/constants';
  25. import {useStructuralSharing} from 'sentry/views/ddm/useStructuralSharing';
  26. interface DDMContextValue {
  27. addFocusArea: (area: FocusArea) => void;
  28. addWidget: () => void;
  29. addWidgets: (widgets: Partial<MetricWidgetQueryParams>[]) => void;
  30. duplicateWidget: (index: number) => void;
  31. focusArea: FocusArea | null;
  32. isLoading: boolean;
  33. metricsMeta: ReturnType<typeof useMetricsMeta>['data'];
  34. removeFocusArea: () => void;
  35. removeWidget: (index: number) => void;
  36. selectedWidgetIndex: number;
  37. setSelectedWidgetIndex: (index: number) => void;
  38. updateWidget: (index: number, data: Partial<MetricWidgetQueryParams>) => void;
  39. widgets: MetricWidgetQueryParams[];
  40. }
  41. export const DDMContext = createContext<DDMContextValue>({
  42. selectedWidgetIndex: 0,
  43. setSelectedWidgetIndex: () => {},
  44. addWidget: () => {},
  45. addWidgets: () => {},
  46. updateWidget: () => {},
  47. removeWidget: () => {},
  48. addFocusArea: () => {},
  49. removeFocusArea: () => {},
  50. duplicateWidget: () => {},
  51. widgets: [],
  52. metricsMeta: [],
  53. isLoading: false,
  54. focusArea: null,
  55. });
  56. export function useDDMContext() {
  57. return useContext(DDMContext);
  58. }
  59. const emptyWidget: MetricWidgetQueryParams = {
  60. mri: 'd:transactions/duration@millisecond' satisfies MRI,
  61. op: 'count',
  62. query: '',
  63. groupBy: [],
  64. sort: DEFAULT_SORT_STATE,
  65. displayType: MetricDisplayType.LINE,
  66. title: undefined,
  67. };
  68. export function useMetricWidgets() {
  69. const router = useRouter();
  70. const updateQuery = useUpdateQuery();
  71. const widgets = useStructuralSharing(
  72. useMemo<MetricWidgetQueryParams[]>(() => {
  73. const currentWidgets = JSON.parse(
  74. router.location.query.widgets ?? JSON.stringify([emptyWidget])
  75. );
  76. return currentWidgets.map((widget: MetricWidgetQueryParams) => {
  77. return {
  78. mri: widget.mri,
  79. op: widget.op,
  80. query: widget.query,
  81. groupBy: decodeList(widget.groupBy),
  82. displayType: widget.displayType ?? defaultMetricDisplayType,
  83. focusedSeries: widget.focusedSeries,
  84. showSummaryTable: widget.showSummaryTable ?? true, // temporary default
  85. powerUserMode: widget.powerUserMode,
  86. sort: widget.sort ?? DEFAULT_SORT_STATE,
  87. title: widget.title,
  88. };
  89. });
  90. }, [router.location.query.widgets])
  91. );
  92. // We want to have it as a ref, so that we can use it in the setWidget callback
  93. // without needing to generate a new callback every time the location changes
  94. const currentWidgetsRef = useInstantRef(widgets);
  95. const setWidgets = useCallback(
  96. (newWidgets: React.SetStateAction<MetricWidgetQueryParams[]>) => {
  97. const currentWidgets = currentWidgetsRef.current;
  98. updateQuery({
  99. widgets: JSON.stringify(
  100. typeof newWidgets === 'function' ? newWidgets(currentWidgets) : newWidgets
  101. ),
  102. });
  103. },
  104. [updateQuery, currentWidgetsRef]
  105. );
  106. const updateWidget = useCallback(
  107. (index: number, data: Partial<MetricWidgetQueryParams>) => {
  108. setWidgets(currentWidgets => {
  109. const newWidgets = [...currentWidgets];
  110. newWidgets[index] = {...currentWidgets[index], ...data};
  111. return newWidgets;
  112. });
  113. },
  114. [setWidgets]
  115. );
  116. const addWidget = useCallback(() => {
  117. setWidgets(currentWidgets => [...currentWidgets, emptyWidget]);
  118. }, [setWidgets]);
  119. const addWidgets = useCallback(
  120. (newWidgets: Partial<MetricWidgetQueryParams>[]) => {
  121. const widgetsCopy = [...widgets].filter(widget => !!widget.mri);
  122. widgetsCopy.push(...newWidgets.map(widget => ({...emptyWidget, ...widget})));
  123. setWidgets(widgetsCopy);
  124. },
  125. [widgets, setWidgets]
  126. );
  127. const removeWidget = useCallback(
  128. (index: number) => {
  129. setWidgets(currentWidgets => {
  130. const newWidgets = [...currentWidgets];
  131. newWidgets.splice(index, 1);
  132. return newWidgets;
  133. });
  134. },
  135. [setWidgets]
  136. );
  137. const duplicateWidget = useCallback(
  138. (index: number) => {
  139. setWidgets(currentWidgets => {
  140. const newWidgets = [...currentWidgets];
  141. newWidgets.splice(index, 0, currentWidgets[index]);
  142. return newWidgets;
  143. });
  144. },
  145. [setWidgets]
  146. );
  147. return {
  148. widgets,
  149. updateWidget,
  150. addWidget,
  151. addWidgets,
  152. removeWidget,
  153. duplicateWidget,
  154. };
  155. }
  156. export function DDMContextProvider({children}: {children: React.ReactNode}) {
  157. const router = useRouter();
  158. const updateQuery = useUpdateQuery();
  159. const [selectedWidgetIndex, setSelectedWidgetIndex] = useState(0);
  160. const {widgets, updateWidget, addWidget, addWidgets, removeWidget, duplicateWidget} =
  161. useMetricWidgets();
  162. const [focusArea, setFocusArea] = useState<FocusArea | null>(null);
  163. const pageFilters = usePageFilters().selection;
  164. const {data: metricsMeta, isLoading} = useMetricsMeta(pageFilters.projects);
  165. const handleAddFocusArea = useCallback(
  166. (area: FocusArea) => {
  167. const dateRange = getAbsoluteDateTimeRange(pageFilters.datetime);
  168. if (!area.range.start || !area.range.end) {
  169. Sentry.metrics.increment('ddm.enhance.range-undefined');
  170. return;
  171. }
  172. if (area.range.start < dateRange.start || area.range.end > dateRange.end) {
  173. Sentry.metrics.increment('ddm.enhance.range-overflow');
  174. return;
  175. }
  176. Sentry.metrics.increment('ddm.enhance.add');
  177. setFocusArea(area);
  178. setSelectedWidgetIndex(area.widgetIndex);
  179. updateQuery({focusArea: JSON.stringify(area)});
  180. },
  181. [updateQuery, pageFilters.datetime]
  182. );
  183. const handleRemoveFocusArea = useCallback(() => {
  184. Sentry.metrics.increment('ddm.enhance.remove');
  185. setFocusArea(null);
  186. updateQuery({focusArea: null});
  187. }, [updateQuery]);
  188. // Load focus area from URL
  189. useEffect(() => {
  190. if (focusArea) {
  191. return;
  192. }
  193. const urlFocusArea = router.location.query.focusArea;
  194. if (urlFocusArea) {
  195. handleAddFocusArea(JSON.parse(urlFocusArea));
  196. }
  197. }, [router, handleAddFocusArea, focusArea]);
  198. const handleAddWidget = useCallback(() => {
  199. addWidget();
  200. setSelectedWidgetIndex(widgets.length);
  201. }, [addWidget, widgets.length]);
  202. const handleUpdateWidget = useCallback(
  203. (index: number, data: Partial<MetricWidgetQueryParams>) => {
  204. updateWidget(index, data);
  205. setSelectedWidgetIndex(index);
  206. if (index === focusArea?.widgetIndex) {
  207. handleRemoveFocusArea();
  208. }
  209. },
  210. [updateWidget, handleRemoveFocusArea, focusArea?.widgetIndex]
  211. );
  212. const handleDuplicate = useCallback(
  213. (index: number) => {
  214. duplicateWidget(index);
  215. setSelectedWidgetIndex(index + 1);
  216. },
  217. [duplicateWidget]
  218. );
  219. const contextValue = useMemo<DDMContextValue>(
  220. () => ({
  221. addWidget: handleAddWidget,
  222. addWidgets,
  223. selectedWidgetIndex:
  224. selectedWidgetIndex > widgets.length - 1 ? 0 : selectedWidgetIndex,
  225. setSelectedWidgetIndex,
  226. updateWidget: handleUpdateWidget,
  227. removeWidget,
  228. duplicateWidget: handleDuplicate,
  229. widgets,
  230. isLoading,
  231. metricsMeta,
  232. focusArea,
  233. addFocusArea: handleAddFocusArea,
  234. removeFocusArea: handleRemoveFocusArea,
  235. }),
  236. [
  237. addWidgets,
  238. handleAddWidget,
  239. handleDuplicate,
  240. handleUpdateWidget,
  241. removeWidget,
  242. isLoading,
  243. metricsMeta,
  244. selectedWidgetIndex,
  245. widgets,
  246. focusArea,
  247. handleAddFocusArea,
  248. handleRemoveFocusArea,
  249. ]
  250. );
  251. return <DDMContext.Provider value={contextValue}>{children}</DDMContext.Provider>;
  252. }