Dashboards: Fix scroll position not being restored when leaving panel edit (#83787)

* Dashboards: Fix scroll position not being restored when leaving panel edit view

* remove mock from tests

* remove console log

* Remove my debugging stuff, and don't render grid if width is 0

* remove old comment (but retain old, probably unneeded css)

* rename ref

* fix it not actually working anymore!!!

* add e2e tests

* jsonnet, i guess
This commit is contained in:
Josh Hunt 2024-03-06 13:57:11 +00:00 committed by GitHub
parent 6db7eafd7e
commit 183aa09eeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1874 additions and 74 deletions

File diff suppressed because it is too large Load Diff

View File

@ -91,6 +91,7 @@
"table_sparkline_cell": (import '../dev-dashboards/panel-table/table_sparkline_cell.json'), "table_sparkline_cell": (import '../dev-dashboards/panel-table/table_sparkline_cell.json'),
"table_tests": (import '../dev-dashboards/panel-table/table_tests.json'), "table_tests": (import '../dev-dashboards/panel-table/table_tests.json'),
"table_tests_new": (import '../dev-dashboards/panel-table/table_tests_new.json'), "table_tests_new": (import '../dev-dashboards/panel-table/table_tests_new.json'),
"tall_dashboard": (import '../dev-dashboards/scenarios/tall_dashboard.json'),
"templating-dashboard-links-and-variables": (import '../dev-dashboards/feature-templating/templating-dashboard-links-and-variables.json'), "templating-dashboard-links-and-variables": (import '../dev-dashboards/feature-templating/templating-dashboard-links-and-variables.json'),
"templating-repeating-panels": (import '../dev-dashboards/feature-templating/templating-repeating-panels.json'), "templating-repeating-panels": (import '../dev-dashboards/feature-templating/templating-repeating-panels.json'),
"templating-repeating-rows": (import '../dev-dashboards/feature-templating/templating-repeating-rows.json'), "templating-repeating-rows": (import '../dev-dashboards/feature-templating/templating-repeating-rows.json'),

View File

@ -0,0 +1,33 @@
import { e2e } from '../utils';
const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard';
describe('Dashboards', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
it('should restore scroll position', () => {
e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST });
e2e.components.Panels.Panel.title('Panel #1').should('be.visible');
// scroll to the bottom
e2e.pages.Dashboard.DashNav.navV2()
.parent()
.parent() // Note, this will probably fail when we change the custom scrollbars
.scrollTo('bottom', {
timeout: 5 * 1000,
});
// The last panel should be visible...
e2e.components.Panels.Panel.title('Panel #50').should('be.visible');
// Then we open and close the panel editor
e2e.components.Panels.Panel.menu('Panel #50').click({ force: true }); // it only shows on hover
e2e.components.Panels.Panel.menuItems('Edit').click();
e2e.components.PanelEditor.applyButton().click();
// And the last panel should still be visible!
e2e.components.Panels.Panel.title('Panel #50').should('be.visible');
});
});

View File

@ -4,7 +4,6 @@ import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { match, Router } from 'react-router-dom'; import { match, Router } from 'react-router-dom';
import { useEffectOnce } from 'react-use'; import { useEffectOnce } from 'react-use';
import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer';
import { mockToolkitActionCreator } from 'test/core/redux/mocks'; import { mockToolkitActionCreator } from 'test/core/redux/mocks';
import { TestProvider } from 'test/helpers/TestProvider'; import { TestProvider } from 'test/helpers/TestProvider';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
@ -71,18 +70,6 @@ jest.mock('@grafana/runtime', () => ({
getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }),
})); }));
jest.mock('react-virtualized-auto-sizer', () => {
// The size of the children need to be small enough to be outside the view.
// So it does not trigger the query to be run by the PanelQueryRunner.
return ({ children }: AutoSizerProps) =>
children({
height: 1,
scaledHeight: 1,
scaledWidth: 1,
width: 1,
});
});
function getTestDashboard(overrides?: Partial<Dashboard>, metaOverrides?: Partial<DashboardMeta>): DashboardModel { function getTestDashboard(overrides?: Partial<Dashboard>, metaOverrides?: Partial<DashboardMeta>): DashboardModel {
const data = Object.assign( const data = Object.assign(
{ {

View File

@ -3,7 +3,6 @@ import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { Router } from 'react-router-dom'; import { Router } from 'react-router-dom';
import { useEffectOnce } from 'react-use'; import { useEffectOnce } from 'react-use';
import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { TextBoxVariableModel } from '@grafana/data'; import { TextBoxVariableModel } from '@grafana/data';
@ -42,18 +41,6 @@ jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => {
return { LazyLoader }; return { LazyLoader };
}); });
jest.mock('react-virtualized-auto-sizer', () => {
// The size of the children need to be small enough to be outside the view.
// So it does not trigger the query to be run by the PanelQueryRunner.
return ({ children }: AutoSizerProps) =>
children({
scaledHeight: 1,
height: 1,
scaledWidth: 1,
width: 1,
});
});
function setup(props: Props) { function setup(props: Props) {
const context = getGrafanaContextMock(); const context = getGrafanaContextMock();
const store = configureStore({}); const store = configureStore({});

View File

@ -1,7 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { PureComponent, CSSProperties } from 'react'; import React, { PureComponent, CSSProperties } from 'react';
import ReactGridLayout, { ItemCallback } from 'react-grid-layout'; import ReactGridLayout, { ItemCallback } from 'react-grid-layout';
import AutoSizer from 'react-virtualized-auto-sizer';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
@ -31,6 +30,7 @@ export interface Props {
interface State { interface State {
panelFilter?: RegExp; panelFilter?: RegExp;
width: number;
} }
export class DashboardGrid extends PureComponent<Props, State> { export class DashboardGrid extends PureComponent<Props, State> {
@ -47,6 +47,7 @@ export class DashboardGrid extends PureComponent<Props, State> {
super(props); super(props);
this.state = { this.state = {
panelFilter: undefined, panelFilter: undefined,
width: document.body.clientWidth, // initial very rough estimate
}; };
} }
@ -291,22 +292,41 @@ export class DashboardGrid extends PureComponent<Props, State> {
} }
}; };
private resizeObserver?: ResizeObserver;
private rootEl: HTMLDivElement | null = null;
onMeasureRef = (rootEl: HTMLDivElement | null) => {
if (!rootEl) {
if (this.rootEl && this.resizeObserver) {
this.resizeObserver.unobserve(this.rootEl);
}
return;
}
this.rootEl = rootEl;
this.resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
this.setState({ width: entry.contentRect.width });
});
});
this.resizeObserver.observe(rootEl);
};
render() { render() {
const { isEditable, dashboard } = this.props; const { isEditable, dashboard } = this.props;
const { width } = this.state;
if (dashboard.panels.length === 0) { if (dashboard.panels.length === 0) {
return <DashboardEmpty dashboard={dashboard} canCreate={isEditable} />; return <DashboardEmpty dashboard={dashboard} canCreate={isEditable} />;
} }
/** const draggable = width <= config.theme2.breakpoints.values.md ? false : isEditable;
* We have a parent with "flex: 1 1 0" we need to reset it to "flex: 1 1 auto" to have the AutoSizer
* properly working. For more information go here: // pos: rel + z-index is required to create a new stacking context to contain
* https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#can-i-use-autosizer-within-a-flex-container // the escalating z-indexes of the panels
*
* pos: rel + z-index is required to create a new stacking context to contain the escalating z-indexes of the panels
*/
return ( return (
<div <div
ref={this.onMeasureRef}
style={{ style={{
flex: '1 1 auto', flex: '1 1 auto',
position: 'relative', position: 'relative',
@ -314,46 +334,27 @@ export class DashboardGrid extends PureComponent<Props, State> {
display: this.props.editPanel ? 'none' : undefined, display: this.props.editPanel ? 'none' : undefined,
}} }}
> >
<AutoSizer disableHeight> <div style={{ width: width, height: '100%' }} ref={this.onGetWrapperDivRef}>
{({ width }) => { <ReactGridLayout
if (width === 0) { width={width}
return null; isDraggable={draggable}
} isResizable={isEditable}
containerPadding={[0, 0]}
// Disable draggable if mobile device, solving an issue with unintentionally useCSSTransforms={true}
// moving panels. https://github.com/grafana/grafana/issues/18497 margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
const draggable = width <= config.theme2.breakpoints.values.md ? false : isEditable; cols={GRID_COLUMN_COUNT}
rowHeight={GRID_CELL_HEIGHT}
return ( draggableHandle=".grid-drag-handle"
/** draggableCancel=".grid-drag-cancel"
* The children is using a width of 100% so we need to guarantee that it is wrapped layout={this.buildLayout()}
* in an element that has the calculated size given by the AutoSizer. The AutoSizer onDragStop={this.onDragStop}
* has a width of 0 and will let its content overflow its div. onResize={this.onResize}
*/ onResizeStop={this.onResizeStop}
<div style={{ width: width, height: '100%' }} ref={this.onGetWrapperDivRef}> onLayoutChange={this.onLayoutChange}
<ReactGridLayout >
width={width} {this.renderPanels(width, draggable)}
isDraggable={draggable} </ReactGridLayout>
isResizable={isEditable} </div>
containerPadding={[0, 0]}
useCSSTransforms={true}
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
cols={GRID_COLUMN_COUNT}
rowHeight={GRID_CELL_HEIGHT}
draggableHandle=".grid-drag-handle"
draggableCancel=".grid-drag-cancel"
layout={this.buildLayout()}
onDragStop={this.onDragStop}
onResize={this.onResize}
onResizeStop={this.onResizeStop}
onLayoutChange={this.onLayoutChange}
>
{this.renderPanels(width, draggable)}
</ReactGridLayout>
</div>
);
}}
</AutoSizer>
</div> </div>
); );
} }