Browse Source

Dashboard: Lazy load out of view panels (#15554)

* try this again

* use element rather than grid position

* adding back console output to debug gridPos alternative

* less logging

* simplify

* subscribe/unsubscribe to event streams when view changes

* Panels: Minor change to lazy loading
Ryan McKinley 6 years ago
parent
commit
c3a5204933

+ 9 - 1
public/app/features/dashboard/containers/DashboardPage.tsx

@@ -270,6 +270,9 @@ export class DashboardPage extends PureComponent<Props, State> {
       'dashboard-container--has-submenu': dashboard.meta.submenuEnabled,
       'dashboard-container--has-submenu': dashboard.meta.submenuEnabled,
     });
     });
 
 
+    // Only trigger render when the scroll has moved by 25
+    const approximateScrollTop = Math.round(scrollTop / 25) * 25;
+
     return (
     return (
       <div className={classes}>
       <div className={classes}>
         <DashNav
         <DashNav
@@ -294,7 +297,12 @@ export class DashboardPage extends PureComponent<Props, State> {
 
 
             <div className={gridWrapperClasses}>
             <div className={gridWrapperClasses}>
               {dashboard.meta.submenuEnabled && <SubMenu dashboard={dashboard} />}
               {dashboard.meta.submenuEnabled && <SubMenu dashboard={dashboard} />}
-              <DashboardGrid dashboard={dashboard} isEditing={isEditing} isFullscreen={isFullscreen} />
+              <DashboardGrid
+                dashboard={dashboard}
+                isEditing={isEditing}
+                isFullscreen={isFullscreen}
+                scrollTop={approximateScrollTop}
+              />
             </div>
             </div>
           </CustomScrollbar>
           </CustomScrollbar>
         </div>
         </div>

+ 1 - 1
public/app/features/dashboard/containers/SoloPanelPage.tsx

@@ -89,7 +89,7 @@ export class SoloPanelPage extends Component<Props, State> {
 
 
     return (
     return (
       <div className="panel-solo">
       <div className="panel-solo">
-        <DashboardPanel dashboard={dashboard} panel={panel} isEditing={false} isFullscreen={false} />
+        <DashboardPanel dashboard={dashboard} panel={panel} isEditing={false} isFullscreen={false} isInView={true} />
       </div>
       </div>
     );
     );
   }
   }

+ 2 - 0
public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap

@@ -211,6 +211,7 @@ exports[`DashboardPage Dashboard init completed  Should render dashboard grid 1`
           }
           }
           isEditing={false}
           isEditing={false}
           isFullscreen={false}
           isFullscreen={false}
+          scrollTop={0}
         />
         />
       </div>
       </div>
     </CustomScrollbar>
     </CustomScrollbar>
@@ -540,6 +541,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
           }
           }
           isEditing={false}
           isEditing={false}
           isFullscreen={false}
           isFullscreen={false}
+          scrollTop={0}
         />
         />
       </div>
       </div>
     </CustomScrollbar>
     </CustomScrollbar>

+ 53 - 4
public/app/features/dashboard/dashgrid/DashboardGrid.tsx

@@ -83,11 +83,12 @@ export interface Props {
   dashboard: DashboardModel;
   dashboard: DashboardModel;
   isEditing: boolean;
   isEditing: boolean;
   isFullscreen: boolean;
   isFullscreen: boolean;
+  scrollTop: number;
 }
 }
 
 
 export class DashboardGrid extends PureComponent<Props> {
 export class DashboardGrid extends PureComponent<Props> {
-  gridToPanelMap: any;
   panelMap: { [id: string]: PanelModel };
   panelMap: { [id: string]: PanelModel };
+  panelRef: { [id: string]: HTMLElement } = {};
 
 
   componentDidMount() {
   componentDidMount() {
     const { dashboard } = this.props;
     const { dashboard } = this.props;
@@ -149,6 +150,9 @@ export class DashboardGrid extends PureComponent<Props> {
     }
     }
 
 
     this.props.dashboard.sortPanelsByGridPos();
     this.props.dashboard.sortPanelsByGridPos();
+
+    // Call render() after any changes.  This is called when the layour loads
+    this.forceUpdate();
   };
   };
 
 
   triggerForceUpdate = () => {
   triggerForceUpdate = () => {
@@ -174,7 +178,6 @@ export class DashboardGrid extends PureComponent<Props> {
   };
   };
 
 
   onResize: ItemCallback = (layout, oldItem, newItem) => {
   onResize: ItemCallback = (layout, oldItem, newItem) => {
-    console.log();
     this.panelMap[newItem.i].updateGridPos(newItem);
     this.panelMap[newItem.i].updateGridPos(newItem);
   };
   };
 
 
@@ -187,18 +190,64 @@ export class DashboardGrid extends PureComponent<Props> {
     this.updateGridPos(newItem, layout);
     this.updateGridPos(newItem, layout);
   };
   };
 
 
+  isInView = (panel: PanelModel): boolean => {
+    if (panel.fullscreen || 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
+      return false;
+    }
+
+    const top = parseInt(elem.style.top.replace('px', ''), 10);
+    const height = panel.gridPos.h * GRID_CELL_HEIGHT + 40;
+    const bottom = top + height;
+
+    // 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) {
+      return false;
+    }
+
+    return !this.props.dashboard.otherPanelInFullscreen(panel);
+  };
+
   renderPanels() {
   renderPanels() {
     const panelElements = [];
     const panelElements = [];
-
     for (const panel of this.props.dashboard.panels) {
     for (const panel of this.props.dashboard.panels) {
       const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.fullscreen });
       const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.fullscreen });
+      const id = panel.id.toString();
       panelElements.push(
       panelElements.push(
-        <div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id}`}>
+        <div
+          key={id}
+          className={panelClasses}
+          id={'panel-' + id}
+          ref={elem => {
+            this.panelRef[id] = elem;
+          }}
+        >
           <DashboardPanel
           <DashboardPanel
             panel={panel}
             panel={panel}
             dashboard={this.props.dashboard}
             dashboard={this.props.dashboard}
             isEditing={panel.isEditing}
             isEditing={panel.isEditing}
             isFullscreen={panel.fullscreen}
             isFullscreen={panel.fullscreen}
+            isInView={this.isInView(panel)}
           />
           />
         </div>
         </div>
       );
       );

+ 16 - 3
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -23,11 +23,13 @@ export interface Props {
   dashboard: DashboardModel;
   dashboard: DashboardModel;
   isEditing: boolean;
   isEditing: boolean;
   isFullscreen: boolean;
   isFullscreen: boolean;
+  isInView: boolean;
 }
 }
 
 
 export interface State {
 export interface State {
   plugin: PanelPlugin;
   plugin: PanelPlugin;
   angularPanel: AngularComponent;
   angularPanel: AngularComponent;
+  isLazy: boolean;
 }
 }
 
 
 export class DashboardPanel extends PureComponent<Props, State> {
 export class DashboardPanel extends PureComponent<Props, State> {
@@ -40,6 +42,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
     this.state = {
     this.state = {
       plugin: null,
       plugin: null,
       angularPanel: null,
       angularPanel: null,
+      isLazy: !props.isInView,
     };
     };
 
 
     this.specialPanels['row'] = this.renderRow.bind(this);
     this.specialPanels['row'] = this.renderRow.bind(this);
@@ -90,7 +93,11 @@ export class DashboardPanel extends PureComponent<Props, State> {
     this.loadPlugin(this.props.panel.type);
     this.loadPlugin(this.props.panel.type);
   }
   }
 
 
-  componentDidUpdate() {
+  componentDidUpdate(prevProps: Props, prevState: State) {
+    if (this.state.isLazy && this.props.isInView) {
+      this.setState({ isLazy: false });
+    }
+
     if (!this.element || this.state.angularPanel) {
     if (!this.element || this.state.angularPanel) {
       return;
       return;
     }
     }
@@ -123,7 +130,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
   };
   };
 
 
   renderReactPanel() {
   renderReactPanel() {
-    const { dashboard, panel, isFullscreen } = this.props;
+    const { dashboard, panel, isFullscreen, isInView } = this.props;
     const { plugin } = this.state;
     const { plugin } = this.state;
 
 
     return (
     return (
@@ -138,6 +145,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
               panel={panel}
               panel={panel}
               dashboard={dashboard}
               dashboard={dashboard}
               isFullscreen={isFullscreen}
               isFullscreen={isFullscreen}
+              isInView={isInView}
               width={width}
               width={width}
               height={height}
               height={height}
             />
             />
@@ -153,7 +161,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
 
 
   render() {
   render() {
     const { panel, dashboard, isFullscreen, isEditing } = this.props;
     const { panel, dashboard, isFullscreen, isEditing } = this.props;
-    const { plugin, angularPanel } = this.state;
+    const { plugin, angularPanel, isLazy } = this.state;
 
 
     if (this.isSpecial(panel.type)) {
     if (this.isSpecial(panel.type)) {
       return this.specialPanels[panel.type]();
       return this.specialPanels[panel.type]();
@@ -164,6 +172,11 @@ export class DashboardPanel extends PureComponent<Props, State> {
       return null;
       return null;
     }
     }
 
 
+    // If we are lazy state don't render anything
+    if (isLazy) {
+      return null;
+    }
+
     const containerClass = classNames({ 'panel-editor-container': isEditing, 'panel-height-helper': !isEditing });
     const containerClass = classNames({ 'panel-editor-container': isEditing, 'panel-height-helper': !isEditing });
     const panelWrapperClass = classNames({
     const panelWrapperClass = classNames({
       'panel-wrapper': true,
       'panel-wrapper': true,

+ 42 - 25
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -31,6 +31,7 @@ export interface Props {
   dashboard: DashboardModel;
   dashboard: DashboardModel;
   plugin: PanelPlugin;
   plugin: PanelPlugin;
   isFullscreen: boolean;
   isFullscreen: boolean;
+  isInView: boolean;
   width: number;
   width: number;
   height: number;
   height: number;
 }
 }
@@ -39,6 +40,7 @@ export interface State {
   isFirstLoad: boolean;
   isFirstLoad: boolean;
   renderCounter: number;
   renderCounter: number;
   errorMessage: string | null;
   errorMessage: string | null;
+  refreshWhenInView: boolean;
 
 
   // Current state of all events
   // Current state of all events
   data: PanelData;
   data: PanelData;
@@ -47,7 +49,6 @@ export interface State {
 export class PanelChrome extends PureComponent<Props, State> {
 export class PanelChrome extends PureComponent<Props, State> {
   timeSrv: TimeSrv = getTimeSrv();
   timeSrv: TimeSrv = getTimeSrv();
   querySubscription: Unsubscribable;
   querySubscription: Unsubscribable;
-  delayedStateUpdate: Partial<State>;
 
 
   constructor(props: Props) {
   constructor(props: Props) {
     super(props);
     super(props);
@@ -55,6 +56,7 @@ export class PanelChrome extends PureComponent<Props, State> {
       isFirstLoad: true,
       isFirstLoad: true,
       renderCounter: 0,
       renderCounter: 0,
       errorMessage: null,
       errorMessage: null,
+      refreshWhenInView: false,
       data: {
       data: {
         state: LoadingState.NotStarted,
         state: LoadingState.NotStarted,
         series: [],
         series: [],
@@ -90,17 +92,46 @@ export class PanelChrome extends PureComponent<Props, State> {
     }
     }
   }
   }
 
 
+  componentDidUpdate(prevProps: Props) {
+    const { isInView } = this.props;
+
+    // View state has changed
+    if (isInView !== prevProps.isInView) {
+      if (isInView) {
+        // Subscribe will kick of a notice of the last known state
+        if (!this.querySubscription && this.wantsQueryExecution) {
+          const runner = this.props.panel.getQueryRunner();
+          this.querySubscription = runner.subscribe(this.panelDataObserver);
+        }
+
+        // Check if we need a delayed refresh
+        if (this.state.refreshWhenInView) {
+          this.onRefresh();
+        }
+      } else if (this.querySubscription) {
+        this.querySubscription.unsubscribe();
+        this.querySubscription = null;
+      }
+    }
+  }
+
   // Updates the response with information from the stream
   // Updates the response with information from the stream
   // The next is outside a react synthetic event so setState is not batched
   // The next is outside a react synthetic event so setState is not batched
   // So in this context we can only do a single call to setState
   // So in this context we can only do a single call to setState
   panelDataObserver = {
   panelDataObserver = {
     next: (data: PanelData) => {
     next: (data: PanelData) => {
+      if (!this.props.isInView) {
+        // Ignore events when not visible.
+        // The call will be repeated when the panel comes into view
+        return;
+      }
+
       let { errorMessage, isFirstLoad } = this.state;
       let { errorMessage, isFirstLoad } = this.state;
 
 
       if (data.state === LoadingState.Error) {
       if (data.state === LoadingState.Error) {
         const { error } = data;
         const { error } = data;
         if (error) {
         if (error) {
-          if (this.state.errorMessage !== error.message) {
+          if (errorMessage !== error.message) {
             errorMessage = error.message;
             errorMessage = error.message;
           }
           }
         }
         }
@@ -113,30 +144,26 @@ export class PanelChrome extends PureComponent<Props, State> {
         if (this.props.dashboard.snapshot) {
         if (this.props.dashboard.snapshot) {
           this.props.panel.snapshotData = data.series;
           this.props.panel.snapshotData = data.series;
         }
         }
-        if (this.state.isFirstLoad) {
+        if (isFirstLoad) {
           isFirstLoad = false;
           isFirstLoad = false;
         }
         }
       }
       }
 
 
-      const stateUpdate = { isFirstLoad, errorMessage, data };
-
-      if (this.isVisible) {
-        this.setState(stateUpdate);
-      } else {
-        // if we are getting data while another panel is in fullscreen / edit mode
-        // we need to store the data but not update state yet
-        this.delayedStateUpdate = stateUpdate;
-      }
+      this.setState({ isFirstLoad, errorMessage, data });
     },
     },
   };
   };
 
 
   onRefresh = () => {
   onRefresh = () => {
-    console.log('onRefresh');
-    if (!this.isVisible) {
+    const { panel, isInView, width } = this.props;
+
+    console.log('onRefresh', panel.id);
+
+    if (!isInView) {
+      console.log('Refresh when panel is visible', panel.id);
+      this.setState({ refreshWhenInView: true });
       return;
       return;
     }
     }
 
 
-    const { panel, width } = this.props;
     const timeData = applyPanelTimeOverrides(panel, this.timeSrv.timeRange());
     const timeData = applyPanelTimeOverrides(panel, this.timeSrv.timeRange());
 
 
     // Issue Query
     // Issue Query
@@ -172,12 +199,6 @@ export class PanelChrome extends PureComponent<Props, State> {
   onRender = () => {
   onRender = () => {
     const stateUpdate = { renderCounter: this.state.renderCounter + 1 };
     const stateUpdate = { renderCounter: this.state.renderCounter + 1 };
 
 
-    // If we have received a data update while hidden copy over that state as well
-    if (this.delayedStateUpdate) {
-      Object.assign(stateUpdate, this.delayedStateUpdate);
-      this.delayedStateUpdate = null;
-    }
-
     this.setState(stateUpdate);
     this.setState(stateUpdate);
   };
   };
 
 
@@ -199,10 +220,6 @@ export class PanelChrome extends PureComponent<Props, State> {
     }
     }
   };
   };
 
 
-  get isVisible() {
-    return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
-  }
-
   get hasPanelSnapshot() {
   get hasPanelSnapshot() {
     const { panel } = this.props;
     const { panel } = this.props;
     return panel.snapshotData && panel.snapshotData.length;
     return panel.snapshotData && panel.snapshotData.length;