context.tsx 7.6 KB

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