1import React, { createContext, Dispatch, PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react';
2import { AnyAction } from '@reduxjs/toolkit';
3import { QueryEditorProps } from '@grafana/data';
4import { GraphiteDatasource } from '../datasource';
5import { GraphiteOptions, GraphiteQuery } from '../types';
6import { createStore, GraphiteQueryEditorState } from './store';
7import { getTemplateSrv } from 'app/features/templating/template_srv';
8import { actions } from './actions';
9import { usePrevious } from 'react-use';
10
11const DispatchContext = createContext<Dispatch<AnyAction>>({} as Dispatch<AnyAction>);
12const GraphiteStateContext = createContext<GraphiteQueryEditorState>({} as GraphiteQueryEditorState);
13
14export const useDispatch = () => {
15  return useContext(DispatchContext);
16};
17
18export const useGraphiteState = () => {
19  return useContext(GraphiteStateContext);
20};
21
22export type GraphiteQueryEditorProps = QueryEditorProps<GraphiteDatasource, GraphiteQuery, GraphiteOptions>;
23
24export const GraphiteQueryEditorContext = ({
25  datasource,
26  onRunQuery,
27  onChange,
28  query,
29  queries,
30  range,
31  children,
32}: PropsWithChildren<GraphiteQueryEditorProps>) => {
33  const [state, setState] = useState<GraphiteQueryEditorState>();
34  const [needsRefresh, setNeedsRefresh] = useState<boolean>(false);
35
36  const dispatch = useMemo(() => {
37    return createStore((state) => {
38      setState(state);
39    });
40  }, []);
41
42  // synchronise changes provided in props with editor's state
43  const previousRange = usePrevious(range);
44  useEffect(() => {
45    if (previousRange?.raw !== range?.raw) {
46      dispatch(actions.timeRangeChanged(range));
47    }
48  }, [dispatch, range, previousRange]);
49
50  useEffect(
51    () => {
52      if (state) {
53        dispatch(actions.queriesChanged(queries));
54      }
55    },
56    // adding state to dependencies causes infinite loops
57    // eslint-disable-next-line react-hooks/exhaustive-deps
58    [dispatch, queries]
59  );
60
61  useEffect(
62    () => {
63      if (state && state.target?.target !== query.target) {
64        dispatch(actions.queryChanged(query));
65      }
66    },
67    // adding state to dependencies causes infinite loops
68    // eslint-disable-next-line react-hooks/exhaustive-deps
69    [dispatch, query]
70  );
71
72  useEffect(
73    () => {
74      if (needsRefresh && state) {
75        setNeedsRefresh(false);
76        onChange({ ...query, target: state.target.target });
77        onRunQuery();
78      }
79    },
80    // adding state to dependencies causes infinite loops
81    // eslint-disable-next-line react-hooks/exhaustive-deps
82    [needsRefresh, onChange, onRunQuery, query]
83  );
84
85  if (!state) {
86    dispatch(
87      actions.init({
88        target: query,
89        datasource: datasource,
90        range: range,
91        templateSrv: getTemplateSrv(),
92        // list of queries is passed only when the editor is in Dashboards. This is to allow interpolation
93        // of sub-queries which are stored in "targetFull" property used by alerting in the backend.
94        queries: queries || [],
95        refresh: () => {
96          // do not run onChange/onRunQuery straight away to ensure the internal state gets updated first
97          // to avoid race conditions (onChange could update props before the reducer action finishes)
98          setNeedsRefresh(true);
99        },
100      })
101    );
102    return null;
103  } else {
104    return (
105      <GraphiteStateContext.Provider value={state}>
106        <DispatchContext.Provider value={dispatch}>{children}</DispatchContext.Provider>
107      </GraphiteStateContext.Provider>
108    );
109  }
110};
111