context.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import {createContext, useCallback, useContext, useMemo, useState} from 'react';
  2. import {MRI} from 'sentry/types';
  3. import {
  4. defaultMetricDisplayType,
  5. MetricDisplayType,
  6. MetricWidgetQueryParams,
  7. updateQuery,
  8. } from 'sentry/utils/metrics';
  9. import {parseMRI} from 'sentry/utils/metrics/mri';
  10. import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
  11. import {decodeList} from 'sentry/utils/queryString';
  12. import usePageFilters from 'sentry/utils/usePageFilters';
  13. import useRouter from 'sentry/utils/useRouter';
  14. import {FocusArea} from 'sentry/views/ddm/chartBrush';
  15. import {DEFAULT_SORT_STATE} from 'sentry/views/ddm/constants';
  16. interface DDMContextValue {
  17. addFocusArea: (area: FocusArea) => void;
  18. addWidget: () => void;
  19. duplicateWidget: (index: number) => void;
  20. focusArea: FocusArea | null;
  21. hasCustomMetrics: boolean;
  22. isLoading: boolean;
  23. metricsMeta: ReturnType<typeof useMetricsMeta>['data'];
  24. removeFocusArea: () => void;
  25. removeWidget: (index: number) => void;
  26. selectedWidgetIndex: number;
  27. setSelectedWidgetIndex: (index: number) => void;
  28. updateWidget: (index: number, data: Partial<MetricWidgetQueryParams>) => void;
  29. widgets: MetricWidgetQueryParams[];
  30. }
  31. export const DDMContext = createContext<DDMContextValue>({
  32. selectedWidgetIndex: 0,
  33. setSelectedWidgetIndex: () => {},
  34. addWidget: () => {},
  35. updateWidget: () => {},
  36. removeWidget: () => {},
  37. addFocusArea: () => {},
  38. removeFocusArea: () => {},
  39. duplicateWidget: () => {},
  40. widgets: [],
  41. metricsMeta: [],
  42. hasCustomMetrics: false,
  43. isLoading: false,
  44. focusArea: null,
  45. });
  46. export function useDDMContext() {
  47. return useContext(DDMContext);
  48. }
  49. const emptyWidget: MetricWidgetQueryParams = {
  50. mri: '' as MRI,
  51. op: undefined,
  52. query: '',
  53. groupBy: [],
  54. sort: DEFAULT_SORT_STATE,
  55. displayType: MetricDisplayType.LINE,
  56. title: undefined,
  57. };
  58. export function useMetricWidgets() {
  59. const router = useRouter();
  60. const widgets = useMemo<MetricWidgetQueryParams[]>(() => {
  61. const currentWidgets = JSON.parse(
  62. router.location.query.widgets ?? JSON.stringify([emptyWidget])
  63. );
  64. return currentWidgets.map((widget: MetricWidgetQueryParams) => {
  65. return {
  66. mri: widget.mri,
  67. op: widget.op,
  68. query: widget.query,
  69. groupBy: decodeList(widget.groupBy),
  70. displayType: widget.displayType ?? defaultMetricDisplayType,
  71. focusedSeries: widget.focusedSeries,
  72. showSummaryTable: widget.showSummaryTable ?? true, // temporary default
  73. powerUserMode: widget.powerUserMode,
  74. sort: widget.sort ?? DEFAULT_SORT_STATE,
  75. title: widget.title,
  76. };
  77. });
  78. }, [router.location.query.widgets]);
  79. const setWidgets = useCallback(
  80. (newWidgets: MetricWidgetQueryParams[]) => {
  81. updateQuery(router, {
  82. widgets: JSON.stringify(newWidgets),
  83. });
  84. },
  85. [router]
  86. );
  87. const updateWidget = useCallback(
  88. (index: number, data: Partial<MetricWidgetQueryParams>) => {
  89. const widgetsCopy = [...widgets];
  90. widgetsCopy[index] = {...widgets[index], ...data};
  91. setWidgets(widgetsCopy);
  92. },
  93. [widgets, setWidgets]
  94. );
  95. const addWidget = useCallback(() => {
  96. const widgetsCopy = [...widgets];
  97. widgetsCopy.push(emptyWidget);
  98. setWidgets(widgetsCopy);
  99. }, [widgets, setWidgets]);
  100. const removeWidget = useCallback(
  101. (index: number) => {
  102. const widgetsCopy = [...widgets];
  103. widgetsCopy.splice(index, 1);
  104. setWidgets(widgetsCopy);
  105. },
  106. [setWidgets, widgets]
  107. );
  108. const duplicateWidget = useCallback(
  109. (index: number) => {
  110. const widgetsCopy = [...widgets];
  111. widgetsCopy.splice(index, 0, widgets[index]);
  112. setWidgets(widgetsCopy);
  113. },
  114. [setWidgets, widgets]
  115. );
  116. return {
  117. widgets,
  118. updateWidget,
  119. addWidget,
  120. removeWidget,
  121. duplicateWidget,
  122. };
  123. }
  124. export function DDMContextProvider({children}: {children: React.ReactNode}) {
  125. const [selectedWidgetIndex, setSelectedWidgetIndex] = useState(0);
  126. const [focusArea, setFocusArea] = useState<FocusArea | null>(null);
  127. const {widgets, updateWidget, addWidget, removeWidget, duplicateWidget} =
  128. useMetricWidgets();
  129. const pageFilters = usePageFilters().selection;
  130. const {data: metricsMeta, isLoading} = useMetricsMeta(pageFilters.projects);
  131. // TODO(telemetry-experience): Switch to the logic below once we have the hasCustomMetrics flag on project
  132. // const {projects} = useProjects();
  133. // const selectedProjects = projects.filter(project =>
  134. // pageFilters.projects.includes(parseInt(project.id, 10))
  135. // );
  136. // const hasCustomMetrics = selectedProjects.some(project => project.hasCustomMetrics);
  137. const hasCustomMetrics = !!metricsMeta.find(
  138. meta => parseMRI(meta)?.useCase === 'custom'
  139. );
  140. const handleAddWidget = useCallback(() => {
  141. addWidget();
  142. setSelectedWidgetIndex(widgets.length);
  143. }, [addWidget, widgets.length]);
  144. const handleUpdateWidget = useCallback(
  145. (index: number, data: Partial<MetricWidgetQueryParams>) => {
  146. updateWidget(index, data);
  147. setSelectedWidgetIndex(index);
  148. },
  149. [updateWidget]
  150. );
  151. const handleDuplicate = useCallback(
  152. (index: number) => {
  153. duplicateWidget(index);
  154. setSelectedWidgetIndex(index + 1);
  155. },
  156. [duplicateWidget]
  157. );
  158. const handleAddFocusArea = useCallback((area: FocusArea) => {
  159. setFocusArea(area);
  160. setSelectedWidgetIndex(area.widgetIndex);
  161. }, []);
  162. const handleRemoveFocusArea = useCallback(() => {
  163. setFocusArea(null);
  164. }, []);
  165. const contextValue = useMemo<DDMContextValue>(
  166. () => ({
  167. addWidget: handleAddWidget,
  168. selectedWidgetIndex:
  169. selectedWidgetIndex > widgets.length - 1 ? 0 : selectedWidgetIndex,
  170. setSelectedWidgetIndex,
  171. updateWidget: handleUpdateWidget,
  172. removeWidget,
  173. duplicateWidget: handleDuplicate,
  174. widgets,
  175. hasCustomMetrics,
  176. isLoading,
  177. metricsMeta,
  178. focusArea,
  179. addFocusArea: handleAddFocusArea,
  180. removeFocusArea: handleRemoveFocusArea,
  181. }),
  182. [
  183. handleAddWidget,
  184. handleDuplicate,
  185. handleUpdateWidget,
  186. removeWidget,
  187. hasCustomMetrics,
  188. isLoading,
  189. metricsMeta,
  190. selectedWidgetIndex,
  191. widgets,
  192. focusArea,
  193. handleAddFocusArea,
  194. handleRemoveFocusArea,
  195. ]
  196. );
  197. return <DDMContext.Provider value={contextValue}>{children}</DDMContext.Provider>;
  198. }