context.tsx 8.7 KB

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