|
|
|
@ -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 {} |
|
|
|
|
|
|
|
|
|
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<typeof connector>; |
|
|
|
|
class WrapperUnconnected extends PureComponent<Props, WrapperState> { |
|
|
|
|
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() { |
|
|
|
|
const MIN_PANE_WIDTH = 200; |
|
|
|
|
function Wrapper(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { |
|
|
|
|
useExplorePageTitle(); |
|
|
|
|
const { maxedExploreId, evenSplitPanes } = useSelector((state) => state.explore); |
|
|
|
|
const [rightPaneWidth, setRightPaneWidth] = useState<number>(); |
|
|
|
|
const [prevWindowWidth, setWindowWidth] = useState<number>(); |
|
|
|
|
const dispatch = useDispatch(); |
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null); |
|
|
|
|
const queryParams = props.queryParams; |
|
|
|
|
const { keybindings, chrome } = useGrafana(); |
|
|
|
|
const navModel = useNavModel('explore'); |
|
|
|
|
|
|
|
|
|
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<Props, WrapperState> { |
|
|
|
|
locationService.partial({ from: undefined, to: undefined }, true); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', this.windowResizeListener); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
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
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
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 - 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 ( |
|
|
|
|
<div className={styles.pageScrollbarWrapper}> |
|
|
|
|
<ExploreActions exploreIdLeft={ExploreId.left} exploreIdRight={ExploreId.right} /> |
|
|
|
|
<div className={styles.exploreWrapper}> |
|
|
|
|
<SplitView uiState={splitSizeObj} minSize={this.minWidth} onResize={this.updateSplitSize}> |
|
|
|
|
<ErrorBoundaryAlert style="page" key="LeftPane"> |
|
|
|
|
<ExplorePaneContainer split={hasSplit} exploreId={ExploreId.left} urlQuery={left} /> |
|
|
|
|
const splitSizeObj = { rightPaneSize: widthCalc }; |
|
|
|
|
return ( |
|
|
|
|
<div className={styles.pageScrollbarWrapper} ref={containerRef}> |
|
|
|
|
<ExploreActions exploreIdLeft={ExploreId.left} exploreIdRight={ExploreId.right} /> |
|
|
|
|
<div className={styles.exploreWrapper}> |
|
|
|
|
<SplitView uiState={splitSizeObj} minSize={MIN_PANE_WIDTH} onResize={updateSplitSize}> |
|
|
|
|
<ErrorBoundaryAlert style="page" key="LeftPane"> |
|
|
|
|
<ExplorePaneContainer split={hasSplit} exploreId={ExploreId.left} urlQuery={queryParams.left} /> |
|
|
|
|
</ErrorBoundaryAlert> |
|
|
|
|
{hasSplit && ( |
|
|
|
|
<ErrorBoundaryAlert style="page" key="RightPane"> |
|
|
|
|
<ExplorePaneContainer split={hasSplit} exploreId={ExploreId.right} urlQuery={queryParams.right} /> |
|
|
|
|
</ErrorBoundaryAlert> |
|
|
|
|
{hasSplit && ( |
|
|
|
|
<ErrorBoundaryAlert style="page" key="RightPane"> |
|
|
|
|
<ExplorePaneContainer split={hasSplit} exploreId={ExploreId.right} urlQuery={right} /> |
|
|
|
|
</ErrorBoundaryAlert> |
|
|
|
|
)} |
|
|
|
|
</SplitView> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
</SplitView> |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|