mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Support remember scroll position when coming back from view panel, panel edit and settings (#92185)
* DashboardScene: Support remember scroll position when coming back from view panel, panel edit and settings * remove unused state prop * Update * Fixes * Update e2e
This commit is contained in:
parent
81ce3c92d5
commit
801f2ba728
@ -24,9 +24,7 @@ describe('Dashboards', () => {
|
|||||||
e2e.components.Panels.Panel.menuItems('Edit').click();
|
e2e.components.Panels.Panel.menuItems('Edit').click();
|
||||||
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
|
e2e.components.NavToolbar.editDashboard.backToDashboardButton().click();
|
||||||
|
|
||||||
// And the last panel should still be visible!
|
// The last panel should still be visible!
|
||||||
// TODO: investigate scroll to on navigating back
|
e2e.components.Panels.Panel.title('Panel #50').should('be.visible');
|
||||||
// e2e.components.Panels.Panel.title('Panel #50').should('be.visible');
|
|
||||||
// e2e.components.Panels.Panel.title('Panel #1').should('be.visible');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,28 +2,35 @@ import { css, cx } from '@emotion/css';
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
type Props = Parameters<typeof CustomScrollbar>[0];
|
export interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onSetScrollRef?: (ref: ScrollRefElement) => void;
|
||||||
|
divId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScrollRefElement {
|
||||||
|
scrollTop: number;
|
||||||
|
scrollTo: (x: number, y: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
// Shim to provide API-compatibility for Page's scroll-related props
|
// Shim to provide API-compatibility for Page's scroll-related props
|
||||||
// when bodyScrolling is enabled, this is a no-op
|
// when bodyScrolling is enabled, this is a no-op
|
||||||
// TODO remove this shim completely when bodyScrolling is enabled
|
// TODO remove this shim completely when bodyScrolling is enabled
|
||||||
export default function NativeScrollbar({ children, scrollRefCallback, scrollTop, divId }: Props) {
|
export default function NativeScrollbar({ children, onSetScrollRef, divId }: Props) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config.featureToggles.bodyScrolling && ref.current && scrollRefCallback) {
|
if (config.featureToggles.bodyScrolling && onSetScrollRef) {
|
||||||
scrollRefCallback(ref.current);
|
onSetScrollRef(new WindowScrollElement());
|
||||||
}
|
}
|
||||||
}, [ref, scrollRefCallback]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (!config.featureToggles.bodyScrolling && ref.current && onSetScrollRef) {
|
||||||
if (!config.featureToggles.bodyScrolling && ref.current && scrollTop != null) {
|
onSetScrollRef(ref.current);
|
||||||
ref.current?.scrollTo(0, scrollTop);
|
|
||||||
}
|
}
|
||||||
}, [scrollTop]);
|
}, [ref, onSetScrollRef]);
|
||||||
|
|
||||||
return config.featureToggles.bodyScrolling ? (
|
return config.featureToggles.bodyScrolling ? (
|
||||||
children
|
children
|
||||||
@ -35,6 +42,16 @@ export default function NativeScrollbar({ children, scrollRefCallback, scrollTop
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WindowScrollElement {
|
||||||
|
public get scrollTop() {
|
||||||
|
return window.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public scrollTo(x: number, y: number) {
|
||||||
|
window.scrollTo(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getStyles() {
|
function getStyles() {
|
||||||
return {
|
return {
|
||||||
nativeScrollbars: css({
|
nativeScrollbars: css({
|
||||||
|
@ -27,8 +27,7 @@ export const Page: PageType = ({
|
|||||||
className,
|
className,
|
||||||
info,
|
info,
|
||||||
layout = PageLayoutType.Standard,
|
layout = PageLayoutType.Standard,
|
||||||
scrollTop,
|
onSetScrollRef,
|
||||||
scrollRef,
|
|
||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@ -57,9 +56,7 @@ export const Page: PageType = ({
|
|||||||
<NativeScrollbar
|
<NativeScrollbar
|
||||||
// This id is used by the image renderer to scroll through the dashboard
|
// This id is used by the image renderer to scroll through the dashboard
|
||||||
divId="page-scrollbar"
|
divId="page-scrollbar"
|
||||||
autoHeightMin={'100%'}
|
onSetScrollRef={onSetScrollRef}
|
||||||
scrollTop={scrollTop}
|
|
||||||
scrollRefCallback={scrollRef}
|
|
||||||
>
|
>
|
||||||
<div className={styles.pageInner}>
|
<div className={styles.pageInner}>
|
||||||
{pageHeaderNav && (
|
{pageHeaderNav && (
|
||||||
@ -82,9 +79,7 @@ export const Page: PageType = ({
|
|||||||
<NativeScrollbar
|
<NativeScrollbar
|
||||||
// This id is used by the image renderer to scroll through the dashboard
|
// This id is used by the image renderer to scroll through the dashboard
|
||||||
divId="page-scrollbar"
|
divId="page-scrollbar"
|
||||||
autoHeightMin={'100%'}
|
onSetScrollRef={onSetScrollRef}
|
||||||
scrollTop={scrollTop}
|
|
||||||
scrollRefCallback={scrollRef}
|
|
||||||
>
|
>
|
||||||
<div className={styles.canvasContent}>{children}</div>
|
<div className={styles.canvasContent}>{children}</div>
|
||||||
</NativeScrollbar>
|
</NativeScrollbar>
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { FC, HTMLAttributes, RefCallback } from 'react';
|
import { FC, HTMLAttributes } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
||||||
|
|
||||||
|
import { ScrollRefElement } from '../NativeScrollbar';
|
||||||
|
|
||||||
import { PageContents } from './PageContents';
|
import { PageContents } from './PageContents';
|
||||||
|
|
||||||
export interface PageProps extends HTMLAttributes<HTMLDivElement> {
|
export interface PageProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
@ -22,15 +24,11 @@ export interface PageProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
/** Control the page layout. */
|
/** Control the page layout. */
|
||||||
layout?: PageLayoutType;
|
layout?: PageLayoutType;
|
||||||
/**
|
/**
|
||||||
|
* TODO: Not sure we should deprecated it given the sidecar project?
|
||||||
* @deprecated this will be removed when bodyScrolling is enabled by default
|
* @deprecated this will be removed when bodyScrolling is enabled by default
|
||||||
* Can be used to get the scroll container element to access scroll position
|
* Can be used to get the scroll container element to access scroll position
|
||||||
* */
|
* */
|
||||||
scrollRef?: RefCallback<HTMLDivElement>;
|
onSetScrollRef?: (ref: ScrollRefElement) => void;
|
||||||
/**
|
|
||||||
* @deprecated this will be removed when bodyScrolling is enabled by default
|
|
||||||
* Can be used to update the current scroll position
|
|
||||||
* */
|
|
||||||
scrollTop?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageInfoItem {
|
export interface PageInfoItem {
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { Dashboard, DashboardLink, LibraryPanel } from '@grafana/schema';
|
import { Dashboard, DashboardLink, LibraryPanel } from '@grafana/schema';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { ScrollRefElement } from 'app/core/components/NativeScrollbar';
|
||||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
@ -167,6 +168,11 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
* A reference to the scopes facade
|
* A reference to the scopes facade
|
||||||
*/
|
*/
|
||||||
private _scopesFacade: ScopesFacade | null;
|
private _scopesFacade: ScopesFacade | null;
|
||||||
|
/**
|
||||||
|
* Remember scroll position when going into panel edit
|
||||||
|
*/
|
||||||
|
private _scrollRef?: ScrollRefElement;
|
||||||
|
private _prevScrollPos?: number;
|
||||||
|
|
||||||
public constructor(state: Partial<DashboardSceneState>) {
|
public constructor(state: Partial<DashboardSceneState>) {
|
||||||
super({
|
super({
|
||||||
@ -925,6 +931,22 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onSetScrollRef = (scrollElement: ScrollRefElement): void => {
|
||||||
|
this._scrollRef = scrollElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
public rememberScrollPos() {
|
||||||
|
this._prevScrollPos = this._scrollRef?.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public restoreScrollPos() {
|
||||||
|
if (this._prevScrollPos !== undefined) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._scrollRef?.scrollTo(0, this._prevScrollPos!);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardVariableDependency implements SceneVariableDependencyConfigLike {
|
export class DashboardVariableDependency implements SceneVariableDependencyConfigLike {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||||
@ -16,7 +17,7 @@ import { DashboardScene } from './DashboardScene';
|
|||||||
import { NavToolbarActions } from './NavToolbarActions';
|
import { NavToolbarActions } from './NavToolbarActions';
|
||||||
|
|
||||||
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
||||||
const { controls, overlay, editview, editPanel, isEmpty, meta } = model.useState();
|
const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene } = model.useState();
|
||||||
const headerHeight = useChromeHeaderHeight();
|
const headerHeight = useChromeHeaderHeight();
|
||||||
const styles = useStyles2(getStyles, headerHeight);
|
const styles = useStyles2(getStyles, headerHeight);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -25,6 +26,21 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
|||||||
const bodyToRender = model.getBodyToRender();
|
const bodyToRender = model.getBodyToRender();
|
||||||
const navModel = getNavModel(navIndex, 'dashboards/browse');
|
const navModel = getNavModel(navIndex, 'dashboards/browse');
|
||||||
const hasControls = controls?.hasControls();
|
const hasControls = controls?.hasControls();
|
||||||
|
const isSettingsOpen = editview !== undefined;
|
||||||
|
|
||||||
|
// Remember scroll pos when going into view panel, edit panel or settings
|
||||||
|
useMemo(() => {
|
||||||
|
if (viewPanelScene || isSettingsOpen || editPanel) {
|
||||||
|
model.rememberScrollPos();
|
||||||
|
}
|
||||||
|
}, [isSettingsOpen, editPanel, viewPanelScene, model]);
|
||||||
|
|
||||||
|
// Restore scroll pos when coming back
|
||||||
|
useEffect(() => {
|
||||||
|
if (!viewPanelScene && !isSettingsOpen && !editPanel) {
|
||||||
|
model.restoreScrollPos();
|
||||||
|
}
|
||||||
|
}, [isSettingsOpen, editPanel, viewPanelScene, model]);
|
||||||
|
|
||||||
if (editview) {
|
if (editview) {
|
||||||
return (
|
return (
|
||||||
@ -54,12 +70,11 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
|||||||
} else if (isEmpty) {
|
} else if (isEmpty) {
|
||||||
body = [emptyState, withPanels];
|
body = [emptyState, withPanels];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
|
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
|
||||||
{editPanel && <editPanel.Component model={editPanel} />}
|
{editPanel && <editPanel.Component model={editPanel} />}
|
||||||
{!editPanel && (
|
{!editPanel && (
|
||||||
<NativeScrollbar divId="page-scrollbar" autoHeightMin={'100%'}>
|
<NativeScrollbar divId="page-scrollbar" onSetScrollRef={model.onSetScrollRef}>
|
||||||
<div className={cx(styles.pageContainer, hasControls && styles.pageContainerWithControls)}>
|
<div className={cx(styles.pageContainer, hasControls && styles.pageContainerWithControls)}>
|
||||||
<NavToolbarActions dashboard={model} />
|
<NavToolbarActions dashboard={model} />
|
||||||
{controls && (
|
{controls && (
|
||||||
|
@ -7,6 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
|||||||
import { config, locationService } from '@grafana/runtime';
|
import { config, locationService } from '@grafana/runtime';
|
||||||
import { Themeable2, withTheme2 } from '@grafana/ui';
|
import { Themeable2, withTheme2 } from '@grafana/ui';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { notifyApp } from 'app/core/actions';
|
||||||
|
import { ScrollRefElement } from 'app/core/components/NativeScrollbar';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||||
import { GrafanaContext, GrafanaContextType } from 'app/core/context/GrafanaContext';
|
import { GrafanaContext, GrafanaContextType } from 'app/core/context/GrafanaContext';
|
||||||
@ -77,7 +78,7 @@ export interface State {
|
|||||||
showLoadingState: boolean;
|
showLoadingState: boolean;
|
||||||
panelNotFound: boolean;
|
panelNotFound: boolean;
|
||||||
editPanelAccessDenied: boolean;
|
editPanelAccessDenied: boolean;
|
||||||
scrollElement?: HTMLDivElement;
|
scrollElement?: ScrollRefElement;
|
||||||
pageNav?: NavModelItem;
|
pageNav?: NavModelItem;
|
||||||
sectionNav?: NavModel;
|
sectionNav?: NavModel;
|
||||||
}
|
}
|
||||||
@ -234,11 +235,9 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
|||||||
locationService.partial({ editPanel: null, viewPanel: null });
|
locationService.partial({ editPanel: null, viewPanel: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.featureToggles.bodyScrolling) {
|
// Update window scroll position
|
||||||
// Update window scroll position
|
if (this.state.updateScrollTop !== undefined && this.state.updateScrollTop !== prevState.updateScrollTop) {
|
||||||
if (this.state.updateScrollTop !== undefined && this.state.updateScrollTop !== prevState.updateScrollTop) {
|
this.state.scrollElement?.scrollTo(0, this.state.updateScrollTop);
|
||||||
window.scrollTo(0, this.state.updateScrollTop);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,19 +262,17 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
const updatedState = { ...state };
|
const updatedState = { ...state };
|
||||||
|
|
||||||
if (config.featureToggles.bodyScrolling) {
|
// Entering settings view
|
||||||
// Entering settings view
|
if (!state.editView && urlEditView) {
|
||||||
if (!state.editView && urlEditView) {
|
updatedState.editView = urlEditView;
|
||||||
updatedState.editView = urlEditView;
|
updatedState.rememberScrollTop = state.scrollElement?.scrollTop;
|
||||||
updatedState.rememberScrollTop = window.scrollY;
|
updatedState.updateScrollTop = 0;
|
||||||
updatedState.updateScrollTop = 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Leaving settings view
|
// Leaving settings view
|
||||||
else if (state.editView && !urlEditView) {
|
else if (state.editView && !urlEditView) {
|
||||||
updatedState.updateScrollTop = state.rememberScrollTop;
|
updatedState.updateScrollTop = state.rememberScrollTop;
|
||||||
updatedState.editView = null;
|
updatedState.editView = null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entering edit mode
|
// Entering edit mode
|
||||||
@ -284,12 +281,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
|||||||
if (panel) {
|
if (panel) {
|
||||||
if (dashboard.canEditPanel(panel)) {
|
if (dashboard.canEditPanel(panel)) {
|
||||||
updatedState.editPanel = panel;
|
updatedState.editPanel = panel;
|
||||||
updatedState.rememberScrollTop = config.featureToggles.bodyScrolling
|
updatedState.rememberScrollTop = state.scrollElement?.scrollTop;
|
||||||
? window.scrollY
|
|
||||||
: state.scrollElement?.scrollTop;
|
|
||||||
if (config.featureToggles.bodyScrolling) {
|
|
||||||
updatedState.updateScrollTop = 0;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
updatedState.editPanelAccessDenied = true;
|
updatedState.editPanelAccessDenied = true;
|
||||||
}
|
}
|
||||||
@ -311,9 +303,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
|||||||
// Should move this state out of dashboard in the future
|
// Should move this state out of dashboard in the future
|
||||||
dashboard.initViewPanel(panel);
|
dashboard.initViewPanel(panel);
|
||||||
updatedState.viewPanel = panel;
|
updatedState.viewPanel = panel;
|
||||||
updatedState.rememberScrollTop = config.featureToggles.bodyScrolling
|
updatedState.rememberScrollTop = state.scrollElement?.scrollTop;
|
||||||
? window.scrollY
|
|
||||||
: state.scrollElement?.scrollTop;
|
|
||||||
updatedState.updateScrollTop = 0;
|
updatedState.updateScrollTop = 0;
|
||||||
} else {
|
} else {
|
||||||
updatedState.panelNotFound = true;
|
updatedState.panelNotFound = true;
|
||||||
@ -337,7 +327,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
|||||||
return updateStatePageNavFromProps(props, updatedState);
|
return updateStatePageNavFromProps(props, updatedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
setScrollRef = (scrollElement: HTMLDivElement): void => {
|
setScrollRef = (scrollElement: ScrollRefElement): void => {
|
||||||
this.setState({ scrollElement });
|
this.setState({ scrollElement });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -366,7 +356,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dashboard, initError, queryParams, theme } = this.props;
|
const { dashboard, initError, queryParams, theme } = this.props;
|
||||||
const { editPanel, viewPanel, updateScrollTop, pageNav, sectionNav } = this.state;
|
const { editPanel, viewPanel, pageNav, sectionNav } = this.state;
|
||||||
const kioskMode = getKioskMode(this.props.queryParams);
|
const kioskMode = getKioskMode(this.props.queryParams);
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
@ -443,8 +433,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
|||||||
pageNav={pageNav}
|
pageNav={pageNav}
|
||||||
layout={PageLayoutType.Canvas}
|
layout={PageLayoutType.Canvas}
|
||||||
className={pageClassName}
|
className={pageClassName}
|
||||||
scrollRef={this.setScrollRef}
|
onSetScrollRef={this.setScrollRef}
|
||||||
scrollTop={updateScrollTop}
|
|
||||||
>
|
>
|
||||||
{showToolbar && (
|
{showToolbar && (
|
||||||
<header data-testid={selectors.pages.Dashboard.DashNav.navV2}>
|
<header data-testid={selectors.pages.Dashboard.DashNav.navV2}>
|
||||||
|
Loading…
Reference in New Issue
Block a user