import { useEffect, useRef } from 'react'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useAppNotification } from 'app/core/copy/appNotification'; import { addListener, ExploreQueryParams, useDispatch, useSelector } from 'app/types'; import { selectPanes } from '../../state/selectors'; import { parseURL } from './parseURL'; import { syncFromURL } from './synchronizer/fromURL'; import { initializeFromURL } from './synchronizer/init'; import { syncToURL, syncToURLPredicate } from './synchronizer/toURL'; export { getUrlStateFromPaneState } from './external.utils'; /** * Bi-directionally syncs URL changes with Explore's state. */ export function useStateSync(params: ExploreQueryParams) { const { location } = useGrafana(); const dispatch = useDispatch(); const panesState = useSelector(selectPanes); const panesStateRef = useRef(panesState); const orgId = useSelector((state) => state.user.orgId); const prevParams = useRef(params); const initState = useRef<'notstarted' | 'pending' | 'done'>('notstarted'); const paused = useRef(false); const { warning } = useAppNotification(); useEffect(() => { // This happens when the user navigates to an explore "empty page" while within Explore. // ie. by clicking on the explore when explore is active. if (!params.panes) { initState.current = 'notstarted'; prevParams.current = params; } }, [params]); useEffect(() => { const unsubscribe = dispatch( addListener({ predicate: (action) => syncToURLPredicate(paused, action), effect: async (_, { cancelActiveListeners, delay, getState }) => { // The following 2 lines will debounce updates to avoid creating history entries when rapid changes // are committed to the store. cancelActiveListeners(); await delay(200); syncToURL(getState().explore, prevParams, initState, location); }, }) ); // @ts-expect-error the return type of addListener is actually callable, but dispatch is not middleware-aware return () => unsubscribe(); }, [dispatch, location]); useEffect(() => { panesStateRef.current = panesState; }, [panesState]); useEffect(() => { const isURLOutOfSync = prevParams.current?.panes !== params.panes; const [urlState, hasParseError] = parseURL(params); hasParseError && warning( 'Could not parse Explore URL', 'The requested URL contains invalid parameters, a default Explore state has been loaded.' ); // This happens when the user first navigates to explore. // Here we want to initialize each pane initial data, wether it comes // from the url or as a result of migrations. if (!isURLOutOfSync && initState.current === 'notstarted') { initState.current = 'pending'; initializeFromURL(urlState, initState, orgId, dispatch, location); } prevParams.current = params; if (isURLOutOfSync && initState.current === 'done') { syncFromURL(urlState, panesStateRef.current, dispatch); } }, [dispatch, orgId, location, params, warning]); }