context.tsx 9.1 KB

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