import React, { Context, createContext, PropsWithChildren, useCallback, useContext, useEffect, useState } from 'react'; import { TimeRange } from '@grafana/data'; import { ElasticDatasource } from '../../datasource'; import { combineReducers, useStatelessReducer, DispatchContext } from '../../hooks/useStatelessReducer'; import { ElasticsearchQuery } from '../../types'; import { createReducer as createBucketAggsReducer } from './BucketAggregationsEditor/state/reducer'; import { reducer as metricsReducer } from './MetricAggregationsEditor/state/reducer'; import { aliasPatternReducer, queryReducer, initQuery } from './state'; const DatasourceContext = createContext(undefined); const QueryContext = createContext(undefined); const RangeContext = createContext(undefined); interface Props { query: ElasticsearchQuery; onChange: (query: ElasticsearchQuery) => void; onRunQuery: () => void; datasource: ElasticDatasource; range: TimeRange; } export const ElasticsearchProvider = ({ children, onChange, onRunQuery, query, datasource, range, }: PropsWithChildren) => { const onStateChange = useCallback( (query: ElasticsearchQuery) => { onChange(query); onRunQuery(); }, [onChange, onRunQuery] ); const reducer = combineReducers>({ query: queryReducer, alias: aliasPatternReducer, metrics: metricsReducer, bucketAggs: createBucketAggsReducer(datasource.timeField), }); const dispatch = useStatelessReducer( // timeField is part of the query model, but its value is always set to be the one from datasource settings. (newState) => onStateChange({ ...query, ...newState, timeField: datasource.timeField }), query, reducer ); const isUninitialized = !query.metrics || !query.bucketAggs || query.query === undefined; const [shouldRunInit, setShouldRunInit] = useState(isUninitialized); // This initializes the query by dispatching an init action to each reducer. // useStatelessReducer will then call `onChange` with the newly generated query useEffect(() => { if (shouldRunInit) { dispatch(initQuery()); setShouldRunInit(false); } }, [shouldRunInit, dispatch]); if (isUninitialized) { return null; } return ( {children} ); }; interface GetHook { (context: Context): () => NonNullable; } const getHook: GetHook = (c) => () => { const contextValue = useContext(c); if (!contextValue) { throw new Error('use ElasticsearchProvider first.'); } return contextValue as NonNullable; }; export const useQuery = getHook(QueryContext); export const useDatasource = getHook(DatasourceContext); export const useRange = getHook(RangeContext);