import { css } from '@emotion/css'; import { inRange } from 'lodash'; import React, { useEffect, useRef, useState } from 'react'; import { useWindowSize } from 'react-use'; import { isTruthy } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { ErrorBoundaryAlert, usePanelContext } from '@grafana/ui'; import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useNavModel } from 'app/core/hooks/useNavModel'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { useDispatch, useSelector } from 'app/types'; import { ExploreId, ExploreQueryParams } from 'app/types/explore'; import { Branding } from '../../core/components/Branding/Branding'; import { useCorrelations } from '../correlations/useCorrelations'; import { ExploreActions } from './ExploreActions'; import { ExplorePaneContainer } from './ExplorePaneContainer'; import { lastSavedUrl, saveCorrelationsAction, resetExploreAction, splitSizeUpdateAction } from './state/main'; const styles = { pageScrollbarWrapper: css` width: 100%; flex-grow: 1; min-height: 0; height: 100%; position: relative; `, }; export function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { useExplorePageTitle(); const dispatch = useDispatch(); const queryParams = props.queryParams; const { keybindings, chrome, config } = useGrafana(); const navModel = useNavModel('explore'); const { get } = useCorrelations(); const { warning } = useAppNotification(); const panelCtx = usePanelContext(); const eventBus = useRef(panelCtx.eventBus.newScopedBus('explore', { onlyLocal: false })); const [rightPaneWidthRatio, setRightPaneWidthRatio] = useState(0.5); const { width: windowWidth } = useWindowSize(); const minWidth = 200; const exploreState = useSelector((state) => state.explore); useEffect(() => { //This is needed for breadcrumbs and topnav. //We should probably abstract this out at some point chrome.update({ sectionNav: navModel.node }); }, [chrome, navModel]); useEffect(() => { keybindings.setupTimeRangeBindings(false); }, [keybindings]); useEffect(() => { if (!config.featureToggles.correlations) { dispatch(saveCorrelationsAction([])); } else { get.execute(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (get.value) { dispatch(saveCorrelationsAction(get.value)); } else if (get.error) { dispatch(saveCorrelationsAction([])); warning( 'Could not load correlations.', 'Correlations data could not be loaded, DataLinks may have partial data.' ); } }, [get.value, get.error, dispatch, warning]); useEffect(() => { lastSavedUrl.left = undefined; lastSavedUrl.right = undefined; // timeSrv (which is used internally) on init reads `from` and `to` param from the URL and updates itself // using those value regardless of what is passed to the init method. // The updated value is then used by Explore to get the range for each pane. // This means that if `from` and `to` parameters are present in the URL, // it would be impossible to change the time range in Explore. // We are only doing this on mount for 2 reasons: // 1: Doing it on update means we'll enter a render loop. // 2: when parsing time in Explore (before feeding it to timeSrv) we make sure `from` is before `to` inside // each pane state in order to not trigger un URL update from timeSrv. const searchParams = locationService.getSearchObject(); if (searchParams.from || searchParams.to) { locationService.partial({ from: undefined, to: undefined }, true); } return () => { // Cleaning up Explore state so that when navigating back to Explore it starts from a blank state dispatch(resetExploreAction()); }; // eslint-disable-next-line react-hooks/exhaustive-deps -- dispatch is stable, doesn't need to be in the deps array }, []); const updateSplitSize = (size: number) => { const evenSplitWidth = windowWidth / 2; const areBothSimilar = inRange(size, evenSplitWidth - 100, evenSplitWidth + 100); if (areBothSimilar) { dispatch(splitSizeUpdateAction({ largerExploreId: undefined })); } else { dispatch( splitSizeUpdateAction({ largerExploreId: size > evenSplitWidth ? ExploreId.right : ExploreId.left, }) ); } setRightPaneWidthRatio(size / windowWidth); }; const hasSplit = Boolean(queryParams.left) && Boolean(queryParams.right); let widthCalc = 0; if (hasSplit) { if (!exploreState.evenSplitPanes && exploreState.maxedExploreId) { widthCalc = exploreState.maxedExploreId === ExploreId.right ? windowWidth - minWidth : minWidth; } else if (exploreState.evenSplitPanes) { widthCalc = Math.floor(windowWidth / 2); } else if (rightPaneWidthRatio !== undefined) { widthCalc = windowWidth * rightPaneWidthRatio; } } return (