context.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. getAbsoluteDateTimeRange,
  13. getDefaultMetricDisplayType,
  14. useInstantRef,
  15. useUpdateQuery,
  16. } from 'sentry/utils/metrics';
  17. import {DEFAULT_SORT_STATE, emptyWidget} from 'sentry/utils/metrics/constants';
  18. import type {MetricWidgetQueryParams} from 'sentry/utils/metrics/types';
  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 type {FocusArea} from 'sentry/views/ddm/focusArea';
  25. import {useStructuralSharing} from 'sentry/views/ddm/useStructuralSharing';
  26. interface DDMContextValue {
  27. addFocusArea: (area: FocusArea) => void;
  28. addWidget: () => void;
  29. duplicateWidget: (index: number) => void;
  30. focusArea: FocusArea | null;
  31. isDefaultQuery: boolean;
  32. isLoading: boolean;
  33. metricsMeta: ReturnType<typeof useMetricsMeta>['data'];
  34. removeFocusArea: () => void;
  35. removeWidget: (index: number) => void;
  36. selectedWidgetIndex: number;
  37. setDefaultQuery: (query: Record<string, any> | null) => void;
  38. setHighlightedSampleId: (sample?: string) => void;
  39. setSelectedWidgetIndex: (index: number) => void;
  40. showQuerySymbols: boolean;
  41. updateWidget: (index: number, data: Partial<MetricWidgetQueryParams>) => void;
  42. widgets: MetricWidgetQueryParams[];
  43. highlightedSampleId?: string;
  44. }
  45. export const DDMContext = createContext<DDMContextValue>({
  46. addFocusArea: () => {},
  47. addWidget: () => {},
  48. duplicateWidget: () => {},
  49. focusArea: null,
  50. isDefaultQuery: false,
  51. isLoading: false,
  52. metricsMeta: [],
  53. removeFocusArea: () => {},
  54. removeWidget: () => {},
  55. selectedWidgetIndex: 0,
  56. setDefaultQuery: () => {},
  57. setSelectedWidgetIndex: () => {},
  58. showQuerySymbols: false,
  59. updateWidget: () => {},
  60. widgets: [],
  61. highlightedSampleId: undefined,
  62. setHighlightedSampleId: () => {},
  63. });
  64. export function useDDMContext() {
  65. return useContext(DDMContext);
  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:
  82. widget.displayType ?? getDefaultMetricDisplayType(widget.mri, widget.op),
  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 => {
  118. const lastWidget = currentWidgets.length
  119. ? currentWidgets[currentWidgets.length - 1]
  120. : {};
  121. const newWidget = {
  122. ...emptyWidget,
  123. ...lastWidget,
  124. };
  125. return [...currentWidgets, newWidget];
  126. });
  127. }, [setWidgets]);
  128. const removeWidget = useCallback(
  129. (index: number) => {
  130. setWidgets(currentWidgets => {
  131. const newWidgets = [...currentWidgets];
  132. newWidgets.splice(index, 1);
  133. return newWidgets;
  134. });
  135. },
  136. [setWidgets]
  137. );
  138. const duplicateWidget = useCallback(
  139. (index: number) => {
  140. setWidgets(currentWidgets => {
  141. const newWidgets = [...currentWidgets];
  142. newWidgets.splice(index, 0, currentWidgets[index]);
  143. return newWidgets;
  144. });
  145. },
  146. [setWidgets]
  147. );
  148. return {
  149. widgets,
  150. updateWidget,
  151. addWidget,
  152. removeWidget,
  153. duplicateWidget,
  154. };
  155. }
  156. const useDefaultQuery = () => {
  157. const router = useRouter();
  158. const [defaultQuery, setDefaultQuery] = useLocalStorageState<Record<
  159. string,
  160. any
  161. > | null>('ddm:default-query', null);
  162. useEffect(() => {
  163. if (defaultQuery && router.location.query.widgets === undefined) {
  164. router.replace({...router.location, query: defaultQuery});
  165. }
  166. // Only call on page load
  167. // eslint-disable-next-line react-hooks/exhaustive-deps
  168. }, []);
  169. return useMemo(
  170. () => ({
  171. defaultQuery,
  172. setDefaultQuery,
  173. isDefaultQuery: !!defaultQuery && isEqual(defaultQuery, router.location.query),
  174. }),
  175. [defaultQuery, router.location.query, setDefaultQuery]
  176. );
  177. };
  178. export function DDMContextProvider({children}: {children: React.ReactNode}) {
  179. const router = useRouter();
  180. const updateQuery = useUpdateQuery();
  181. const {setDefaultQuery, isDefaultQuery} = useDefaultQuery();
  182. const [selectedWidgetIndex, setSelectedWidgetIndex] = useState(0);
  183. const {widgets, updateWidget, addWidget, removeWidget, duplicateWidget} =
  184. useMetricWidgets();
  185. const [focusArea, setFocusArea] = useState<FocusArea | null>(null);
  186. const [highlightedSampleId, setHighlightedSampleId] = useState<string | undefined>();
  187. const pageFilters = usePageFilters().selection;
  188. const {data: metricsMeta, isLoading} = useMetricsMeta(pageFilters.projects);
  189. const handleAddFocusArea = useCallback(
  190. (area: FocusArea) => {
  191. if (!area.range.start || !area.range.end) {
  192. Sentry.metrics.increment('ddm.enhance.range-undefined');
  193. return;
  194. }
  195. const dateRange = getAbsoluteDateTimeRange(pageFilters.datetime);
  196. if (area.range.start < dateRange.start) {
  197. area.range.start = dateRange.start;
  198. }
  199. if (area.range.end > dateRange.end) {
  200. area.range.end = dateRange.end;
  201. }
  202. Sentry.metrics.increment('ddm.enhance.add');
  203. setFocusArea(area);
  204. setSelectedWidgetIndex(area.widgetIndex);
  205. updateQuery({focusArea: JSON.stringify(area)});
  206. },
  207. [updateQuery, pageFilters.datetime]
  208. );
  209. const handleRemoveFocusArea = useCallback(() => {
  210. Sentry.metrics.increment('ddm.enhance.remove');
  211. setFocusArea(null);
  212. updateQuery({focusArea: null});
  213. }, [updateQuery]);
  214. // Load focus area from URL
  215. useEffect(() => {
  216. if (focusArea) {
  217. return;
  218. }
  219. const urlFocusArea = router.location.query.focusArea;
  220. if (urlFocusArea) {
  221. handleAddFocusArea(JSON.parse(urlFocusArea));
  222. }
  223. }, [router, handleAddFocusArea, focusArea]);
  224. const handleAddWidget = useCallback(() => {
  225. addWidget();
  226. setSelectedWidgetIndex(widgets.length);
  227. }, [addWidget, widgets.length]);
  228. const handleUpdateWidget = useCallback(
  229. (index: number, data: Partial<MetricWidgetQueryParams>) => {
  230. updateWidget(index, data);
  231. setSelectedWidgetIndex(index);
  232. if (index === focusArea?.widgetIndex) {
  233. handleRemoveFocusArea();
  234. }
  235. },
  236. [updateWidget, handleRemoveFocusArea, focusArea?.widgetIndex]
  237. );
  238. const handleDuplicate = useCallback(
  239. (index: number) => {
  240. duplicateWidget(index);
  241. setSelectedWidgetIndex(index + 1);
  242. },
  243. [duplicateWidget]
  244. );
  245. const contextValue = useMemo<DDMContextValue>(
  246. () => ({
  247. addWidget: handleAddWidget,
  248. selectedWidgetIndex:
  249. selectedWidgetIndex > widgets.length - 1 ? 0 : selectedWidgetIndex,
  250. setSelectedWidgetIndex,
  251. updateWidget: handleUpdateWidget,
  252. removeWidget,
  253. duplicateWidget: handleDuplicate,
  254. widgets,
  255. isLoading,
  256. metricsMeta,
  257. focusArea,
  258. addFocusArea: handleAddFocusArea,
  259. removeFocusArea: handleRemoveFocusArea,
  260. setDefaultQuery,
  261. isDefaultQuery,
  262. showQuerySymbols: widgets.length > 1,
  263. highlightedSampleId,
  264. setHighlightedSampleId,
  265. }),
  266. [
  267. handleAddWidget,
  268. selectedWidgetIndex,
  269. widgets,
  270. handleUpdateWidget,
  271. removeWidget,
  272. handleDuplicate,
  273. isLoading,
  274. metricsMeta,
  275. focusArea,
  276. handleAddFocusArea,
  277. handleRemoveFocusArea,
  278. setDefaultQuery,
  279. isDefaultQuery,
  280. highlightedSampleId,
  281. setHighlightedSampleId,
  282. ]
  283. );
  284. return <DDMContext.Provider value={contextValue}>{children}</DDMContext.Provider>;
  285. }