diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 58a15ac5790..568c7ba1ede 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -253,7 +253,7 @@ export class PanelEditorUnconnected extends PureComponent { panel={panel} isEditing={true} isViewing={false} - isInView={true} + lazy={false} width={panelSize.width} height={panelSize.height} skipStateCleanUp={true} diff --git a/public/app/features/dashboard/containers/DashboardPage.test.tsx b/public/app/features/dashboard/containers/DashboardPage.test.tsx index 1c14842c54a..79eb8da8a0e 100644 --- a/public/app/features/dashboard/containers/DashboardPage.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { render, screen } from '@testing-library/react'; import { Props, UnthemedDashboardPage } from './DashboardPage'; +import { Props as LazyLoaderProps } from '../dashgrid/LazyLoader'; import { Router } from 'react-router-dom'; import { locationService, setDataSourceSrv } from '@grafana/runtime'; import { DashboardModel } from '../state'; @@ -14,6 +15,13 @@ import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps import { createTheme } from '@grafana/data'; import { AutoSizerProps } from 'react-virtualized-auto-sizer'; +jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => { + const LazyLoader = ({ children }: Pick) => { + return <>{typeof children === 'function' ? children({ isInView: true }) : children}; + }; + return { LazyLoader }; +}); + jest.mock('app/features/dashboard/components/DashboardSettings/GeneralSettings', () => { class GeneralSettings extends React.Component<{}, {}> { render() { diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 94f154f562f..968d74b7c61 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { connect, ConnectedProps } from 'react-redux'; import { locationService } from '@grafana/runtime'; import { selectors } from '@grafana/e2e-selectors'; -import { CustomScrollbar, ScrollbarPosition, stylesFactory, Themeable2, withTheme2 } from '@grafana/ui'; +import { CustomScrollbar, stylesFactory, Themeable2, withTheme2 } from '@grafana/ui'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { Branding } from 'app/core/components/Branding/Branding'; @@ -75,7 +75,6 @@ export type Props = Themeable2 & export interface State { editPanel: PanelModel | null; viewPanel: PanelModel | null; - scrollTop: number; updateScrollTop?: number; rememberScrollTop: number; showLoadingState: boolean; @@ -92,7 +91,6 @@ export class UnthemedDashboardPage extends PureComponent { editPanel: null, viewPanel: null, showLoadingState: false, - scrollTop: 0, rememberScrollTop: 0, panelNotFound: false, editPanelAccessDenied: false, @@ -252,7 +250,6 @@ export class UnthemedDashboardPage extends PureComponent { return { ...state, viewPanel: panel, - rememberScrollTop: state.scrollTop, updateScrollTop: 0, }; } @@ -273,10 +270,6 @@ export class UnthemedDashboardPage extends PureComponent { return state; } - setScrollTop = ({ scrollTop }: ScrollbarPosition): void => { - this.setState({ scrollTop, updateScrollTop: undefined }); - }; - onAddPanel = () => { const { dashboard } = this.props; @@ -320,7 +313,7 @@ export class UnthemedDashboardPage extends PureComponent { render() { const { dashboard, isInitSlow, initError, queryParams, theme } = this.props; - const { editPanel, viewPanel, scrollTop, updateScrollTop } = this.state; + const { editPanel, viewPanel, updateScrollTop } = this.state; const kioskMode = getKioskMode(queryParams.kiosk); const styles = getStyles(theme, kioskMode); @@ -332,8 +325,6 @@ export class UnthemedDashboardPage extends PureComponent { return null; } - // Only trigger render when the scroll has moved by 25 - const approximateScrollTop = Math.round(scrollTop / 25) * 25; const inspectPanel = this.getInspectPanel(); const containerClassNames = classnames(styles.dashboardContainer, { 'panel-in-fullscreen': viewPanel, @@ -361,7 +352,6 @@ export class UnthemedDashboardPage extends PureComponent {
{ )} - +
diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index 7591fe88e2b..b03acf813bd 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -115,7 +115,7 @@ export const SoloPanel = ({ dashboard, notFound, panel, panelId }: SoloPanelProp panel={panel} isEditing={false} isViewing={false} - isInView={true} + lazy={false} /> ); }} diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx index 3b45858b8b5..891a0f0a1f6 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx @@ -3,6 +3,13 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { DashboardGrid, Props } from './DashboardGrid'; import { DashboardModel } from '../state'; +jest.mock('app/features/dashboard/dashgrid/LazyLoader', () => { + const LazyLoader: React.FC = ({ children }) => { + return <>{children}; + }; + return { LazyLoader }; +}); + interface ScenarioContext { props: Props; wrapper?: ShallowWrapper; @@ -59,7 +66,6 @@ function dashboardGridScenario(description: string, scenarioFn: (ctx: ScenarioCo props: { editPanel: null, viewPanel: null, - scrollTop: 0, dashboard: getTestDashboard(), }, setProps: (props: Partial) => { diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index f37f9c32fce..34a5eeb6926 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -21,7 +21,6 @@ export interface Props { dashboard: DashboardModel; editPanel: PanelModel | null; viewPanel: PanelModel | null; - scrollTop: number; } export interface State { @@ -125,32 +124,6 @@ export class DashboardGrid extends PureComponent { this.updateGridPos(newItem, layout); }; - isInView(panel: PanelModel, gridWidth: number) { - if (panel.isViewing || panel.isEditing) { - return true; - } - - const scrollTop = this.props.scrollTop; - const screenPos = this.getPanelScreenPos(panel, gridWidth); - - // Show things that are almost in the view - const buffer = 100; - - // The panel is above the viewport - if (scrollTop > screenPos.bottom + buffer) { - return false; - } - - const scrollViewBottom = scrollTop + this.windowHeight; - - // Panel is below view - if (screenPos.top > scrollViewBottom + buffer) { - return false; - } - - return !this.props.dashboard.otherPanelInFullscreen(panel); - } - getPanelScreenPos(panel: PanelModel, gridWidth: number): { top: number; bottom: number } { let top = 0; @@ -185,9 +158,6 @@ export class DashboardGrid extends PureComponent { for (const panel of this.props.dashboard.panels) { const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.isViewing }); - // Update is in view state - panel.isInView = this.isInView(panel, gridWidth); - panelElements.push( { dashboard={this.props.dashboard} isEditing={panel.isEditing} isViewing={panel.isViewing} - isInView={panel.isInView} width={width} height={height} /> @@ -235,13 +204,14 @@ export class DashboardGrid extends PureComponent { render() { const { dashboard } = this.props; + + /** + * 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: + * https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#can-i-use-autosizer-within-a-flex-container + */ return ( - /** - * 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: - * https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#can-i-use-autosizer-within-a-flex-container - */ -
+
{({ width }) => { if (width === 0) { diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 5755110a058..668ed6e8d42 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -7,6 +7,7 @@ import { StoreState } from 'app/types'; import { PanelPlugin } from '@grafana/data'; import { cleanUpPanelState, setPanelInstanceState } from '../../panel/state/reducers'; import { initPanelState } from '../../panel/state/actions'; +import { LazyLoader } from './LazyLoader'; export interface OwnProps { panel: PanelModel; @@ -14,14 +15,10 @@ export interface OwnProps { dashboard: DashboardModel; isEditing: boolean; isViewing: boolean; - isInView: boolean; width: number; height: number; skipStateCleanUp?: boolean; -} - -export interface State { - isLazy: boolean; + lazy?: boolean; } const mapStateToProps = (state: StoreState, props: OwnProps) => { @@ -46,18 +43,15 @@ const connector = connect(mapStateToProps, mapDispatchToProps); export type Props = OwnProps & ConnectedProps; -export class DashboardPanelUnconnected extends PureComponent { - specialPanels: { [key: string]: Function } = {}; - - constructor(props: Props) { - super(props); +export class DashboardPanelUnconnected extends PureComponent { + static defaultProps: Partial = { + lazy: true, + }; - this.state = { - isLazy: !props.isInView, - }; - } + specialPanels: { [key: string]: Function } = {}; componentDidMount() { + this.props.panel.isInView = !this.props.lazy; if (!this.props.plugin) { this.props.initPanelState(this.props.panel); } @@ -70,21 +64,19 @@ export class DashboardPanelUnconnected extends PureComponent { } } - componentDidUpdate() { - if (this.state.isLazy && this.props.isInView) { - this.setState({ isLazy: false }); - } - } - onInstanceStateChange = (value: any) => { this.props.setPanelInstanceState({ key: this.props.stateKey, value }); }; + onVisibilityChange = (v: boolean) => { + this.props.panel.isInView = v; + }; + renderPanel(plugin: PanelPlugin) { - const { dashboard, panel, isViewing, isInView, isEditing, width, height } = this.props; + const { dashboard, panel, isViewing, isEditing, width, height, lazy } = this.props; - if (plugin.angularPanelCtrl) { - return ( + const renderPanelChrome = (isInView: boolean) => + plugin.angularPanelCtrl ? ( { width={width} height={height} /> + ) : ( + ); - } - return ( - + return lazy ? ( + + {({ isInView }) => renderPanelChrome(isInView)} + + ) : ( + renderPanelChrome(true) ); } render() { const { plugin } = this.props; - const { isLazy } = this.state; // If we have not loaded plugin exports yet, wait if (!plugin) { return null; } - // If we are lazy state don't render anything - if (isLazy) { - return null; - } - return this.renderPanel(plugin); } } diff --git a/public/app/features/dashboard/dashgrid/LazyLoader.tsx b/public/app/features/dashboard/dashgrid/LazyLoader.tsx new file mode 100644 index 00000000000..b1b81b3f23f --- /dev/null +++ b/public/app/features/dashboard/dashgrid/LazyLoader.tsx @@ -0,0 +1,57 @@ +import React, { useRef, useState } from 'react'; +import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId'; +import { useEffectOnce } from 'react-use'; + +export interface Props { + children: React.ReactNode | (({ isInView }: { isInView: boolean }) => React.ReactNode); + width?: number; + height?: number; + onLoad?: () => void; + onChange?: (isInView: boolean) => void; +} + +export function LazyLoader({ children, width, height, onLoad, onChange }: Props) { + const id = useUniqueId(); + const [loaded, setLoaded] = useState(false); + const [isInView, setIsInView] = useState(false); + const wrapperRef = useRef(null); + useEffectOnce(() => { + LazyLoader.addCallback(id, (entry) => { + if (!loaded && entry.isIntersecting) { + setLoaded(true); + onLoad?.(); + } + + setIsInView(entry.isIntersecting); + onChange?.(entry.isIntersecting); + }); + + if (wrapperRef.current) { + LazyLoader.observer.observe(wrapperRef.current); + } + + return () => { + delete LazyLoader.callbacks[id]; + if (Object.keys(LazyLoader.callbacks).length === 0) { + LazyLoader.observer.disconnect(); + } + }; + }); + + return ( +
+ {loaded && (typeof children === 'function' ? children({ isInView }) : children)} +
+ ); +} + +LazyLoader.callbacks = {} as Record void>; +LazyLoader.addCallback = (id: string, c: (e: IntersectionObserverEntry) => void) => (LazyLoader.callbacks[id] = c); +LazyLoader.observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + LazyLoader.callbacks[entry.target.id](entry); + } + }, + { rootMargin: '100px' } +); 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 34968349e38..8431540227c 100644 --- a/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap +++ b/public/app/features/dashboard/dashgrid/__snapshots__/DashboardGrid.test.tsx.snap @@ -4,6 +4,7 @@ exports[`DashboardGrid Can render dashboard grid Should render 1`] = `