DashboardGrid: Refactorings and performance improvements (#35942)

* add back only re-render dashboard first time on layout change

* remove panel refs

* Updates

* Improved is in view logic

* Updates

* Remove unnessary auto sizer

* Found another approach that works with resize as well

* Updates

* fixing test

* Fixing ref issues

* Fixed mobile size handling, and view panel handling, removed now unnessary logic

* Updated snapshot
This commit is contained in:
Torkel Ödegaard 2021-06-22 14:44:18 +02:00 committed by GitHub
parent fcb4e5a211
commit f0cac149da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 231 additions and 1357 deletions

View File

@ -275,7 +275,7 @@
"react-beautiful-dnd": "13.0.0",
"react-diff-viewer": "^3.1.1",
"react-dom": "17.0.1",
"react-grid-layout": "1.2.0",
"react-grid-layout": "1.2.5",
"react-highlight-words": "0.17.0",
"react-loadable": "5.5.0",
"react-popper": "2.2.4",
@ -283,7 +283,6 @@
"react-reverse-portal": "^2.0.1",
"react-router-dom": "^5.2.0",
"react-select": "4.3.0",
"react-sizeme": "2.6.12",
"react-split-pane": "0.1.89",
"react-transition-group": "4.4.1",
"react-use": "13.27.0",

View File

@ -245,6 +245,8 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
isEditing={true}
isViewing={false}
isInView={true}
width={width}
height={height}
/>
</div>
</div>

View File

@ -113,6 +113,10 @@ describe('SoloPanelPage', () => {
soloPanelPageScenario('Dashboard init completed ', (ctx) => {
ctx.setup(() => {
// Needed for AutoSizer to work in test
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 });
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 });
ctx.mount();
ctx.setDashboard();
expect(ctx.dashboard).not.toBeNull();

View File

@ -1,12 +1,9 @@
// Libraries
import React, { Component } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
// Components
import AutoSizer from 'react-virtualized-auto-sizer';
import { DashboardPanel } from '../dashgrid/DashboardPanel';
// Redux
import { initDashboard } from '../state/initDashboard';
// Types
import { StoreState } from 'app/types';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
@ -87,9 +84,24 @@ export class SoloPanelPage extends Component<Props, State> {
}
return (
<div className="panel-solo">
<DashboardPanel dashboard={dashboard} panel={panel} isEditing={false} isViewing={false} isInView={true} />
</div>
<AutoSizer className="panel-solo">
{({ width, height }) => {
if (width === 0) {
return null;
}
return (
<DashboardPanel
width={width}
height={height}
dashboard={dashboard}
panel={panel}
isEditing={false}
isViewing={false}
isInView={true}
/>
);
}}
</AutoSizer>
);
}
}

View File

@ -1,9 +1,8 @@
// Libraries
import React, { PureComponent } from 'react';
import React, { PureComponent, CSSProperties } from 'react';
import ReactGridLayout, { ItemCallback } from 'react-grid-layout';
import classNames from 'classnames';
// @ts-ignore
import sizeMe from 'react-sizeme';
import AutoSizer from 'react-virtualized-auto-sizer';
// Components
import { AddPanelWidget } from '../components/AddPanelWidget';
@ -15,79 +14,8 @@ import { DashboardPanel } from './DashboardPanel';
import { DashboardModel, PanelModel } from '../state';
import { Subscription } from 'rxjs';
import { DashboardPanelsChangedEvent } from 'app/types/events';
let lastGridWidth = 1200;
let ignoreNextWidthChange = false;
interface GridWrapperProps {
size: { width: number };
layout: ReactGridLayout.Layout[];
onLayoutChange: (layout: ReactGridLayout.Layout[]) => void;
children: JSX.Element | JSX.Element[];
onDragStop: ItemCallback;
onResize: ItemCallback;
onResizeStop: ItemCallback;
className: string;
isResizable?: boolean;
isDraggable?: boolean;
viewPanel: PanelModel | null;
}
function GridWrapper({
size,
layout,
onLayoutChange,
children,
onDragStop,
onResize,
onResizeStop,
className,
isResizable,
isDraggable,
viewPanel,
}: GridWrapperProps) {
const width = size.width > 0 ? size.width : lastGridWidth;
// logic to ignore width changes (optimization)
if (width !== lastGridWidth) {
if (ignoreNextWidthChange) {
ignoreNextWidthChange = false;
} else if (!viewPanel && Math.abs(width - lastGridWidth) > 8) {
lastGridWidth = width;
}
}
/*
Disable draggable if mobile device, solving an issue with unintentionally
moving panels. https://github.com/grafana/grafana/issues/18497
theme.breakpoints.md = 769
*/
const draggable = width <= 769 ? false : isDraggable;
return (
<ReactGridLayout
width={lastGridWidth}
className={className}
isDraggable={draggable}
isResizable={isResizable}
containerPadding={[0, 0]}
useCSSTransforms={false}
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
cols={GRID_COLUMN_COUNT}
rowHeight={GRID_CELL_HEIGHT}
draggableHandle=".grid-drag-handle"
layout={layout}
onResize={onResize}
onResizeStop={onResizeStop}
onDragStop={onDragStop}
onLayoutChange={onLayoutChange}
>
{children}
</ReactGridLayout>
);
}
const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
import { GridPos } from '../state/PanelModel';
import { config } from '@grafana/runtime';
export interface Props {
dashboard: DashboardModel;
@ -100,10 +28,12 @@ export interface Props {
export interface State {
isLayoutInitialized: boolean;
}
export class DashboardGrid extends PureComponent<Props, State> {
private panelMap: { [id: string]: PanelModel } = {};
private panelRef: { [id: string]: HTMLElement } = {};
private eventSubs = new Subscription();
private windowHeight = 1200;
private gridWidth = 0;
constructor(props: Props) {
super(props);
@ -162,7 +92,11 @@ export class DashboardGrid extends PureComponent<Props, State> {
}
this.props.dashboard.sortPanelsByGridPos();
this.forceUpdate();
// This is called on grid mount as it can correct invalid initial grid positions
if (!this.state.isLayoutInitialized) {
this.setState({ isLayoutInitialized: true });
}
};
triggerForceUpdate = () => {
@ -185,98 +119,184 @@ export class DashboardGrid extends PureComponent<Props, State> {
this.updateGridPos(newItem, layout);
};
isInView = (panel: PanelModel): boolean => {
isInView(panel: PanelModel) {
if (panel.isViewing || panel.isEditing) {
return true;
}
// elem is set *after* the first render
const elem = this.panelRef[panel.id.toString()];
if (!elem) {
// NOTE the gridPos is also not valid until after the first render
// since it is passed to the layout engine and made to be valid
// for example, you can have Y=0 for everything and it will stack them
// down vertically in the second call
const scrollTop = this.props.scrollTop;
const panelTop = panel.gridPos.y * (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN);
const panelBottom = panelTop + panel.gridPos.h * (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN) - GRID_CELL_VMARGIN;
// Show things that are almost in the view
const buffer = 100;
// The panel is above the viewport
if (scrollTop > panelBottom + buffer) {
return false;
}
const top = elem.offsetTop;
const height = panel.gridPos.h * GRID_CELL_HEIGHT + 40;
const bottom = top + height;
const scrollViewBottom = scrollTop + this.windowHeight;
// Show things that are almost in the view
const buffer = 250;
const viewTop = this.props.scrollTop;
if (viewTop > bottom + buffer) {
return false; // The panel is above the viewport
}
// Use the whole browser height (larger than real value)
// TODO? is there a better way
const viewHeight = isNaN(window.innerHeight) ? (window as any).clientHeight : window.innerHeight;
const viewBot = viewTop + viewHeight;
if (top > viewBot + buffer) {
// Panel is below view
if (panelTop > scrollViewBottom + buffer) {
return false;
}
return !this.props.dashboard.otherPanelInFullscreen(panel);
};
}
renderPanels() {
renderPanels(gridWidth: number) {
const panelElements = [];
// This is to avoid layout re-flows, accessing window.innerHeight can trigger re-flow
// We assume here that if width change height might have changed as well
if (this.gridWidth !== gridWidth) {
this.windowHeight = window.innerHeight ?? 1000;
this.gridWidth = gridWidth;
}
for (const panel of this.props.dashboard.panels) {
const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.isViewing });
const id = panel.id.toString();
const itemKey = panel.id.toString();
// Update is in view state
panel.isInView = this.isInView(panel);
panelElements.push(
<div key={id} className={panelClasses} data-panelid={id} ref={(elem) => elem && (this.panelRef[id] = elem)}>
{this.renderPanel(panel)}
</div>
<GrafanaGridItem
key={itemKey}
className={panelClasses}
data-panelid={itemKey}
gridPos={panel.gridPos}
gridWidth={gridWidth}
windowHeight={this.windowHeight}
isViewing={panel.isViewing}
>
{(width: number, height: number) => {
return this.renderPanel(panel, width, height, itemKey);
}}
</GrafanaGridItem>
);
}
return panelElements;
}
renderPanel(panel: PanelModel) {
renderPanel(panel: PanelModel, width: any, height: any, itemKey: string) {
if (panel.type === 'row') {
return <DashboardRow panel={panel} dashboard={this.props.dashboard} />;
return <DashboardRow key={itemKey} panel={panel} dashboard={this.props.dashboard} />;
}
if (panel.type === 'add-panel') {
return <AddPanelWidget panel={panel} dashboard={this.props.dashboard} />;
return <AddPanelWidget key={itemKey} panel={panel} dashboard={this.props.dashboard} />;
}
return (
<DashboardPanel
key={itemKey}
panel={panel}
dashboard={this.props.dashboard}
isEditing={panel.isEditing}
isViewing={panel.isViewing}
isInView={panel.isInView}
width={width}
height={height}
/>
);
}
render() {
const { dashboard, viewPanel } = this.props;
const { dashboard } = this.props;
const autoSizerStyle: CSSProperties = {
width: '100%',
height: '100%',
};
return (
<SizedReactLayoutGrid
className={classNames({ layout: true })}
layout={this.buildLayout()}
isResizable={dashboard.meta.canEdit}
isDraggable={dashboard.meta.canEdit}
onLayoutChange={this.onLayoutChange}
onDragStop={this.onDragStop}
onResize={this.onResize}
onResizeStop={this.onResizeStop}
viewPanel={viewPanel}
>
{this.renderPanels()}
</SizedReactLayoutGrid>
<AutoSizer style={autoSizerStyle} disableHeight>
{({ width }) => {
if (width === 0) {
return null;
}
const draggable = width <= 769 ? false : dashboard.meta.canEdit;
/*
Disable draggable if mobile device, solving an issue with unintentionally
moving panels. https://github.com/grafana/grafana/issues/18497
theme.breakpoints.md = 769
*/
return (
<ReactGridLayout
width={width}
isDraggable={draggable}
isResizable={dashboard.meta.canEdit}
containerPadding={[0, 0]}
useCSSTransforms={false}
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
cols={GRID_COLUMN_COUNT}
rowHeight={GRID_CELL_HEIGHT}
draggableHandle=".grid-drag-handle"
layout={this.buildLayout()}
onDragStop={this.onDragStop}
onResize={this.onResize}
onResizeStop={this.onResizeStop}
onLayoutChange={this.onLayoutChange}
>
{this.renderPanels(width)}
</ReactGridLayout>
);
}}
</AutoSizer>
);
}
}
interface GrafanaGridItemProps extends Record<string, any> {
gridWidth?: number;
gridPos?: GridPos;
isViewing: string;
windowHeight: number;
children: any;
}
/**
* A hacky way to intercept the react-layout-grid item dimensions and pass them to DashboardPanel
*/
const GrafanaGridItem = React.forwardRef<HTMLDivElement, GrafanaGridItemProps>((props, ref) => {
const theme = config.theme2;
let width = 100;
let height = 100;
const { gridWidth, gridPos, isViewing, windowHeight, ...divProps } = props;
const style: CSSProperties = props.style ?? {};
if (isViewing) {
width = props.gridWidth!;
height = windowHeight * 0.85;
style.height = height;
style.width = '100%';
} else if (props.gridWidth! < theme.breakpoints.values.md) {
width = props.gridWidth!;
height = props.gridPos!.h * (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN) - GRID_CELL_VMARGIN;
style.height = height;
style.width = '100%';
} else {
// RGL passes width and height directly to children as style props.
width = parseFloat(props.style.width);
height = parseFloat(props.style.height);
}
// props.children[0] is our main children. RGL adds the drag handle at props.children[1]
return (
<div {...divProps} ref={ref}>
{/* Pass width and height to children as render props */}
{[props.children[0](width, height), props.children.slice(1)]}
</div>
);
});
GrafanaGridItem.displayName = 'GridItemWithDimensions';

View File

@ -1,7 +1,5 @@
// Libraries
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
import { connect, ConnectedProps } from 'react-redux';
// Components
@ -15,8 +13,6 @@ import { initDashboardPanel } from '../state/actions';
import { DashboardModel, PanelModel } from '../state';
import { StoreState } from 'app/types';
import { PanelPlugin } from '@grafana/data';
import { stylesFactory } from '@grafana/ui';
import { css } from 'emotion';
export interface OwnProps {
panel: PanelModel;
@ -24,6 +20,8 @@ export interface OwnProps {
isEditing: boolean;
isViewing: boolean;
isInView: boolean;
width: number;
height: number;
}
export interface State {
@ -69,51 +67,40 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
}
renderPanel(plugin: PanelPlugin) {
const { dashboard, panel, isViewing, isInView, isEditing } = this.props;
const { dashboard, panel, isViewing, isInView, isEditing, width, height } = this.props;
if (plugin.angularPanelCtrl) {
return (
<PanelChromeAngular
plugin={plugin}
panel={panel}
dashboard={dashboard}
isViewing={isViewing}
isEditing={isEditing}
isInView={isInView}
width={width}
height={height}
/>
);
}
return (
<AutoSizer>
{({ width, height }) => {
if (width === 0) {
return null;
}
if (plugin.angularPanelCtrl) {
return (
<PanelChromeAngular
plugin={plugin}
panel={panel}
dashboard={dashboard}
isViewing={isViewing}
isEditing={isEditing}
isInView={isInView}
width={width}
height={height}
/>
);
}
return (
<PanelChrome
plugin={plugin}
panel={panel}
dashboard={dashboard}
isViewing={isViewing}
isEditing={isEditing}
isInView={isInView}
width={width}
height={height}
/>
);
}}
</AutoSizer>
<PanelChrome
plugin={plugin}
panel={panel}
dashboard={dashboard}
isViewing={isViewing}
isEditing={isEditing}
isInView={isInView}
width={width}
height={height}
/>
);
}
render() {
const { isViewing, plugin } = this.props;
const { plugin } = this.props;
const { isLazy } = this.state;
const styles = getStyles();
// If we have not loaded plugin exports yet, wait
if (!plugin) {
@ -125,27 +112,8 @@ export class DashboardPanelUnconnected extends PureComponent<Props, State> {
return null;
}
return (
<div
className={isViewing === true ? classNames(styles.panelWrapper, styles.panelWrapperView) : styles.panelWrapper}
>
{this.renderPanel(plugin)}
</div>
);
return this.renderPanel(plugin);
}
}
export const getStyles = stylesFactory(() => {
return {
panelWrapper: css`
height: 100%;
position: relative;
`,
panelWrapperView: css`
flex: 1 1 0;
height: 90%;
`,
};
});
export const DashboardPanel = connector(DashboardPanelUnconnected);

View File

@ -38,6 +38,7 @@ import {
} from './getPanelOptionsWithDefaults';
import { QueryGroupOptions } from 'app/types';
import { PanelModelLibraryPanel } from '../../library-panels/types';
export interface GridPos {
x: number;
y: number;

View File

@ -17,10 +17,6 @@
}
.panel-in-fullscreen {
.react-grid-layout {
height: 90% !important;
}
.react-grid-item {
display: none !important;
transition-property: none !important;
@ -28,8 +24,6 @@
&--fullscreen {
display: block !important;
position: unset !important;
width: 100% !important;
height: 100% !important;
transform: translate(0px, 0px) !important;
}
}
@ -54,15 +48,10 @@
}
@include media-breakpoint-down(sm) {
.react-grid-layout {
height: 100% !important;
}
.react-grid-item {
display: block !important;
transition-property: none !important;
position: unset !important;
width: 100% !important;
transform: translate(0px, 0px) !important;
margin-bottom: $space-md;
}

View File

@ -75,6 +75,8 @@ div.flot-text {
margin: 0;
left: 0;
top: 0;
width: '100%';
height: '100%';
.panel-container {
border: none;

View File

@ -7367,6 +7367,11 @@ classnames@2.2.6, classnames@2.x, classnames@^2.2.1, classnames@^2.2.5, classnam
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
classnames@2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
clean-css@4.2.x, clean-css@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
@ -9537,7 +9542,7 @@ elegant-spinner@^1.0.1:
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
element-resize-detector@^1.2.1, element-resize-detector@^1.2.2:
element-resize-detector@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.2.tgz#bf7c3ff915957e4e62e86241ed2f9c86b078892b"
integrity sha512-+LOXRkCJc4I5WhEJxIDjhmE3raF8jtOMBDqSCgZTMz2TX3oXAX5pE2+MDeopJlGdXzP7KzPbBJaUGfNaP9HG4A==
@ -18480,16 +18485,16 @@ react-from-dom@^0.6.0:
resolved "https://registry.yarnpkg.com/react-from-dom/-/react-from-dom-0.6.0.tgz#8b9710ba7fbd36cde9e13e9eb26f5f67ab933678"
integrity sha512-W5m1pYV7qlc9bmpA7p2K/wspYNlAh3aqJ9Tc5KRXe6vt/JlX6/84ol+RQlCMK69z+5e38sOpoVW5i4Qpqgs+EA==
react-grid-layout@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.2.0.tgz#87124d549c86c8df8841666618c8c3e3cb205c26"
integrity sha512-fJMGQFguphkAs0NsLNf8hz9cUv9B642JYei2yddiPby/X/kJ4HFIaMUhhqg1ArVfn/vHet1+h+LE4n85cFPh+Q==
react-grid-layout@1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.2.5.tgz#fa40288d5a1fa783484c44ce78b1e10eb5313d26"
integrity sha512-P/NNWAExTX/zEq+RUh6hrIG67UBicDNCOOg9LZe8BAtSdYtCnCGgVmWBS+sIbM0C8RJIiyGsFHh5dIfCddhS/w==
dependencies:
classnames "2.x"
classnames "2.3.1"
lodash.isequal "^4.0.0"
prop-types "^15.0.0"
react-draggable "^4.0.0"
react-resizable "^1.10.0"
react-resizable "^3.0.1"
react-helmet-async@^1.0.7:
version "1.0.9"
@ -18649,10 +18654,10 @@ react-refresh@^0.8.3:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
react-resizable@^1.10.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.11.0.tgz#0b237c4aff16937b7663de1045861749683227ad"
integrity sha512-VoGz2ddxUFvildS8r8/29UZJeyiM3QJnlmRZSuXm+FpTqq/eIrMPc796Y9XQLg291n2hFZJtIoP1xC3hSTw/jg==
react-resizable@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.4.tgz#aa20108eff28c52c6fddaa49abfbef8abf5e581b"
integrity sha512-StnwmiESiamNzdRHbSSvA65b0ZQJ7eVQpPusrSmcpyGKzC0gojhtO62xxH6YOBmepk9dQTBi9yxidL3W4s3EBA==
dependencies:
prop-types "15.x"
react-draggable "^4.0.3"
@ -18733,16 +18738,6 @@ react-shallow-renderer@^16.13.1:
object-assign "^4.1.1"
react-is "^16.12.0 || ^17.0.0"
react-sizeme@2.6.12:
version "2.6.12"
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.12.tgz#ed207be5476f4a85bf364e92042520499455453e"
integrity sha512-tL4sCgfmvapYRZ1FO2VmBmjPVzzqgHA7kI8lSJ6JS6L78jXFNRdOZFpXyK6P1NBZvKPPCZxReNgzZNUajAerZw==
dependencies:
element-resize-detector "^1.2.1"
invariant "^2.2.4"
shallowequal "^1.1.0"
throttle-debounce "^2.1.0"
react-sizeme@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-3.0.1.tgz#4d12f4244e0e6a0fb97253e7af0314dc7c83a5a0"