diff --git a/.betterer.results b/.betterer.results index 925d7652935..4ba3eb8a341 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4416,8 +4416,7 @@ exports[`better eslint`] = { "public/app/features/explore/state/main.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/features/explore/state/query.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/public/app/features/explore/ExplorePaneContainer.tsx b/public/app/features/explore/ExplorePaneContainer.tsx index 49d12f76d57..b9a946c33c8 100644 --- a/public/app/features/explore/ExplorePaneContainer.tsx +++ b/public/app/features/explore/ExplorePaneContainer.tsx @@ -26,7 +26,7 @@ import { getFiscalYearStartMonth, getTimeZone } from '../profile/state/selectors import Explore from './Explore'; import { initializeExplore, refreshExplore } from './state/explorePane'; -import { lastSavedUrl, cleanupPaneAction, stateSave } from './state/main'; +import { lastSavedUrl, stateSave } from './state/main'; import { importQueries } from './state/query'; import { loadAndInitDatasource } from './state/utils'; @@ -178,7 +178,6 @@ function mapStateToProps(state: StoreState, props: OwnProps) { const mapDispatchToProps = { initializeExplore, refreshExplore, - cleanupPaneAction, importQueries, stateSave, }; diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index 9a33de82a4d..85c49964d66 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -1,28 +1,23 @@ import { css } from '@emotion/css'; +import { useResizeObserver } from '@react-aria/utils'; import { debounce, inRange } from 'lodash'; -import React, { PureComponent } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { locationService } from '@grafana/runtime'; import { ErrorBoundaryAlert } from '@grafana/ui'; import { SplitView } from 'app/core/components/SplitPaneWrapper/SplitView'; -import { GrafanaContext } from 'app/core/context/GrafanaContext'; +import { useGrafana } from 'app/core/context/GrafanaContext'; +import { useNavModel } from 'app/core/hooks/useNavModel'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { StoreState } from 'app/types'; +import { isTruthy } from 'app/core/utils/types'; +import { useSelector, useDispatch } from 'app/types'; import { ExploreId, ExploreQueryParams } from 'app/types/explore'; import { Branding } from '../../core/components/Branding/Branding'; -import { getNavModel } from '../../core/selectors/navModel'; import { ExploreActions } from './ExploreActions'; import { ExplorePaneContainer } from './ExplorePaneContainer'; -import { - lastSavedUrl, - resetExploreAction, - richHistoryUpdatedAction, - cleanupPaneAction, - splitSizeUpdateAction, -} from './state/main'; +import { lastSavedUrl, resetExploreAction, splitSizeUpdateAction } from './state/main'; const styles = { pageScrollbarWrapper: css` @@ -36,64 +31,29 @@ const styles = { `, }; -interface RouteProps extends GrafanaRouteComponentProps<{}, ExploreQueryParams> {} -interface OwnProps {} +const MIN_PANE_WIDTH = 200; +function Wrapper(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { + useExplorePageTitle(); + const { maxedExploreId, evenSplitPanes } = useSelector((state) => state.explore); + const [rightPaneWidth, setRightPaneWidth] = useState(); + const [prevWindowWidth, setWindowWidth] = useState(); + const dispatch = useDispatch(); + const containerRef = useRef(null); + const queryParams = props.queryParams; + const { keybindings, chrome } = useGrafana(); + const navModel = useNavModel('explore'); -interface WrapperState { - rightPaneWidth?: number; - windowWidth?: number; -} - -const mapStateToProps = (state: StoreState) => { - return { - navModel: getNavModel(state.navIndex, 'explore'), - exploreState: state.explore, - }; -}; - -const mapDispatchToProps = { - resetExploreAction, - richHistoryUpdatedAction, - cleanupPaneAction, - splitSizeUpdateAction, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); - -type Props = OwnProps & RouteProps & ConnectedProps; -class WrapperUnconnected extends PureComponent { - minWidth = 200; - static contextType = GrafanaContext; - - constructor(props: Props) { - super(props); - this.state = { - rightPaneWidth: undefined, - windowWidth: undefined, - }; - } - - componentWillUnmount() { - const { left, right } = this.props.queryParams; - this.props.resetExploreAction({}); - - if (Boolean(left)) { - this.props.cleanupPaneAction({ exploreId: ExploreId.left }); - } - - if (Boolean(right)) { - this.props.cleanupPaneAction({ exploreId: ExploreId.right }); - } - - window.removeEventListener('resize', this.windowResizeListener); - } - - componentDidMount() { + useEffect(() => { //This is needed for breadcrumbs and topnav. //We should probably abstract this out at some point - this.context.chrome.update({ sectionNav: this.props.navModel.node }); - this.context.keybindings.setupTimeRangeBindings(false); + chrome.update({ sectionNav: navModel.node }); + }, [chrome, navModel]); + useEffect(() => { + keybindings.setupTimeRangeBindings(false); + }, [keybindings]); + + useEffect(() => { lastSavedUrl.left = undefined; lastSavedUrl.right = undefined; @@ -111,90 +71,101 @@ class WrapperUnconnected extends PureComponent { locationService.partial({ from: undefined, to: undefined }, true); } - window.addEventListener('resize', this.windowResizeListener); - } + 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 + }, []); - componentDidUpdate() { - const { left, right } = this.props.queryParams; - const hasSplit = Boolean(left) && Boolean(right); - const datasourceTitle = hasSplit - ? `${this.props.exploreState.left.datasourceInstance?.name} | ${this.props.exploreState.right?.datasourceInstance?.name}` - : `${this.props.exploreState.left.datasourceInstance?.name}`; - const documentTitle = `${this.props.navModel.main.text} - ${datasourceTitle} - ${Branding.AppTitle}`; - document.title = documentTitle; - } - - windowResizeListener = debounce(() => { + const debouncedFunctionRef = useRef((prevWindowWidth?: number, rightPaneWidth?: number) => { let rightPaneRatio = 0.5; - const windowWidth = window.innerWidth; + if (!containerRef.current) { + return; + } + const windowWidth = containerRef.current.clientWidth; // get the ratio of the previous rightPane to the window width - if (this.state.rightPaneWidth && this.state.windowWidth) { - rightPaneRatio = this.state.rightPaneWidth / this.state.windowWidth; + if (rightPaneWidth && prevWindowWidth) { + rightPaneRatio = rightPaneWidth / prevWindowWidth; } let newRightPaneWidth = Math.floor(windowWidth * rightPaneRatio); - if (newRightPaneWidth < this.minWidth) { + if (newRightPaneWidth < MIN_PANE_WIDTH) { // if right pane is too narrow, make min width - newRightPaneWidth = this.minWidth; - } else if (windowWidth - newRightPaneWidth < this.minWidth) { + newRightPaneWidth = MIN_PANE_WIDTH; + } else if (windowWidth - newRightPaneWidth < MIN_PANE_WIDTH) { // if left pane is too narrow, make right pane = window - minWidth - newRightPaneWidth = windowWidth - this.minWidth; + newRightPaneWidth = windowWidth - MIN_PANE_WIDTH; } - this.setState({ windowWidth, rightPaneWidth: newRightPaneWidth }); - }, 500); + setRightPaneWidth(newRightPaneWidth); + setWindowWidth(windowWidth); + }); - updateSplitSize = (rightPaneWidth: number) => { + // eslint needs the callback to be inline to analyze the dependencies, but we need to use debounce from lodash + // eslint-disable-next-line react-hooks/exhaustive-deps + const onResize = useCallback( + debounce(() => debouncedFunctionRef.current(prevWindowWidth, rightPaneWidth), 500), + [prevWindowWidth, rightPaneWidth] + ); + + const updateSplitSize = (rightPaneWidth: number) => { const evenSplitWidth = window.innerWidth / 2; const areBothSimilar = inRange(rightPaneWidth, evenSplitWidth - 100, evenSplitWidth + 100); if (areBothSimilar) { - this.props.splitSizeUpdateAction({ largerExploreId: undefined }); + dispatch(splitSizeUpdateAction({ largerExploreId: undefined })); } else { - this.props.splitSizeUpdateAction({ - largerExploreId: rightPaneWidth > evenSplitWidth ? ExploreId.right : ExploreId.left, - }); + dispatch( + splitSizeUpdateAction({ + largerExploreId: rightPaneWidth > evenSplitWidth ? ExploreId.right : ExploreId.left, + }) + ); } - this.setState({ ...this.state, rightPaneWidth }); + setRightPaneWidth(rightPaneWidth); }; - render() { - const { left, right } = this.props.queryParams; - const { maxedExploreId, evenSplitPanes } = this.props.exploreState; - const hasSplit = Boolean(left) && Boolean(right); - let widthCalc = 0; + useResizeObserver({ onResize, ref: containerRef }); + const hasSplit = Boolean(queryParams.left) && Boolean(queryParams.right); + let widthCalc = 0; - if (hasSplit) { - if (!evenSplitPanes && maxedExploreId) { - widthCalc = maxedExploreId === ExploreId.right ? window.innerWidth - this.minWidth : this.minWidth; - } else if (evenSplitPanes) { - widthCalc = Math.floor(window.innerWidth / 2); - } else if (this.state.rightPaneWidth !== undefined) { - widthCalc = this.state.rightPaneWidth; - } + if (hasSplit) { + if (!evenSplitPanes && maxedExploreId) { + widthCalc = maxedExploreId === ExploreId.right ? window.innerWidth - MIN_PANE_WIDTH : MIN_PANE_WIDTH; + } else if (evenSplitPanes) { + widthCalc = Math.floor(window.innerWidth / 2); + } else if (rightPaneWidth !== undefined) { + widthCalc = rightPaneWidth; } - - const splitSizeObj = { rightPaneSize: widthCalc }; - - return ( -
- -
- - - - - {hasSplit && ( - - - - )} - -
-
- ); } + + const splitSizeObj = { rightPaneSize: widthCalc }; + return ( +
+ +
+ + + + + {hasSplit && ( + + + + )} + +
+
+ ); } -const Wrapper = connector(WrapperUnconnected); +const useExplorePageTitle = () => { + const navModel = useNavModel('explore'); + const datasources = useSelector((state) => + [state.explore.left.datasourceInstance?.name, state.explore.right?.datasourceInstance?.name].filter(isTruthy) + ); + + const documentTitle = `${navModel.main.text} - ${datasources.join(' | ')} - ${Branding.AppTitle}`; + document.title = documentTitle; +}; export default Wrapper; diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 51946988a6d..58dc80a408e 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -192,8 +192,8 @@ export function initializeExplore( */ export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): ThunkResult { return async (dispatch, getState) => { - const itemState = getState().explore[exploreId]!; - if (!itemState.initialized) { + const itemState = getState().explore[exploreId]; + if (!itemState?.initialized) { return; } diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 6e74afdc048..641bdbcaae7 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -50,10 +50,7 @@ export const evenPaneResizeAction = createAction('explore/evenPaneResizeAction') /** * Resets state for explore. */ -export interface ResetExplorePayload { - force?: boolean; -} -export const resetExploreAction = createAction('explore/resetExplore'); +export const resetExploreAction = createAction('explore/resetExplore'); /** * Close the split view and save URL state. @@ -63,17 +60,6 @@ export interface SplitCloseActionPayload { } export const splitCloseAction = createAction('explore/splitClose'); -/** - * Cleans up a pane state. Could seem like this should be in explorePane.ts actions but in case we area closing - * left pane we need to move right state to the left. - * Also this may seem redundant as we have splitClose actions which clears up state but that action is not called on - * URL change. - */ -export interface CleanupPanePayload { - exploreId: ExploreId; -} -export const cleanupPaneAction = createAction('explore/cleanupPane'); - // // Action creators // @@ -237,30 +223,6 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction): }; } - if (cleanupPaneAction.match(action)) { - const { exploreId } = action.payload as CleanupPanePayload; - - // We want to do this only when we remove single pane not when we are unmounting whole explore. - // It needs to be checked like this because in component we don't get new path (which would tell us if we are - // navigating out of explore) before the unmount. - if (!state[exploreId]?.initialized) { - return state; - } - - if (exploreId === ExploreId.left) { - return { - ...state, - [ExploreId.left]: state[ExploreId.right]!, - [ExploreId.right]: undefined, - }; - } else { - return { - ...state, - [ExploreId.right]: undefined, - }; - } - } - if (syncTimesAction.match(action)) { return { ...state, syncedTimes: action.payload.syncedTimes }; } @@ -287,7 +249,6 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction): } if (resetExploreAction.match(action)) { - const payload: ResetExplorePayload = action.payload; const leftState = state[ExploreId.left]; const rightState = state[ExploreId.right]; stopQueryState(leftState.querySubscription); @@ -295,10 +256,6 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction): stopQueryState(rightState.querySubscription); } - if (payload.force) { - return initialExploreState; - } - return { ...initialExploreState, left: {