diff --git a/package.json b/package.json index a3608995e0b..6d096548aa7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 79402b77bca..cc6c6ea409e 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -245,6 +245,8 @@ export class PanelEditorUnconnected extends PureComponent { isEditing={true} isViewing={false} isInView={true} + width={width} + height={height} /> diff --git a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx index 184f283419e..9ae03c233cb 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx @@ -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(); diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index 78046f961f1..084475a99c8 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -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 { } return ( -
- -
+ + {({ width, height }) => { + if (width === 0) { + return null; + } + return ( + + ); + }} + ); } } diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index fbc56b92ffb..58e88b34eb1 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -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 ( - - {children} - - ); -} - -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 { 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 { } 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 { 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( -
elem && (this.panelRef[id] = elem)}> - {this.renderPanel(panel)} -
+ + {(width: number, height: number) => { + return this.renderPanel(panel, width, height, itemKey); + }} + ); } return panelElements; } - renderPanel(panel: PanelModel) { + renderPanel(panel: PanelModel, width: any, height: any, itemKey: string) { if (panel.type === 'row') { - return ; + return ; } if (panel.type === 'add-panel') { - return ; + return ; } return ( ); } render() { - const { dashboard, viewPanel } = this.props; + const { dashboard } = this.props; + + const autoSizerStyle: CSSProperties = { + width: '100%', + height: '100%', + }; + return ( - - {this.renderPanels()} - + + {({ 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 ( + + {this.renderPanels(width)} + + ); + }} + ); } } + +interface GrafanaGridItemProps extends Record { + 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((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 ( +
+ {/* Pass width and height to children as render props */} + {[props.children[0](width, height), props.children.slice(1)]} +
+ ); +}); + +GrafanaGridItem.displayName = 'GridItemWithDimensions'; diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 93e29b0d8b2..9cda9be168a 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -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 { } 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 ( + + ); + } return ( - - {({ width, height }) => { - if (width === 0) { - return null; - } - - if (plugin.angularPanelCtrl) { - return ( - - ); - } - - return ( - - ); - }} - + ); } 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 { return null; } - return ( -
- {this.renderPanel(plugin)} -
- ); + 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); diff --git a/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap b/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap index d2704b9d6ec..bff9bbbda0c 100644 --- a/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap +++ b/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap @@ -1,1135 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DashboardGrid Can render dashboard grid Should render 1`] = ` - -
- -
-
- -
-
- -
-
- -
-
+ + `; diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 65383643ad3..1b9bb9db61e 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -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; diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index c83bc9997ea..e1f067f3911 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -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; } diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 3099f41a999..952322f3301 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -75,6 +75,8 @@ div.flot-text { margin: 0; left: 0; top: 0; + width: '100%'; + height: '100%'; .panel-container { border: none; diff --git a/yarn.lock b/yarn.lock index f64841610a3..ded3a294682 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"