Skip to content

Commit

Permalink
Dashboard: Lazy load out of view panels (grafana#15554)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ryantxu authored and torkelo committed May 3, 2019
1 parent 846b932 commit c3a5204
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 34 deletions.
10 changes: 9 additions & 1 deletion public/app/features/dashboard/containers/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ export class DashboardPage extends PureComponent<Props, State> {
'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 (
<div className={classes}>
<DashNav
Expand All @@ -294,7 +297,12 @@ export class DashboardPage extends PureComponent<Props, State> {

<div className={gridWrapperClasses}>
{dashboard.meta.submenuEnabled && <SubMenu dashboard={dashboard} />}
<DashboardGrid dashboard={dashboard} isEditing={isEditing} isFullscreen={isFullscreen} />
<DashboardGrid
dashboard={dashboard}
isEditing={isEditing}
isFullscreen={isFullscreen}
scrollTop={approximateScrollTop}
/>
</div>
</CustomScrollbar>
</div>
Expand Down
2 changes: 1 addition & 1 deletion public/app/features/dashboard/containers/SoloPanelPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class SoloPanelPage extends Component<Props, State> {

return (
<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>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
}
isEditing={false}
isFullscreen={false}
scrollTop={0}
/>
</div>
</CustomScrollbar>
Expand Down Expand Up @@ -540,6 +541,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
}
isEditing={false}
isFullscreen={false}
scrollTop={0}
/>
</div>
</CustomScrollbar>
Expand Down
57 changes: 53 additions & 4 deletions public/app/features/dashboard/dashgrid/DashboardGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ export interface Props {
dashboard: DashboardModel;
isEditing: boolean;
isFullscreen: boolean;
scrollTop: number;
}

export class DashboardGrid extends PureComponent<Props> {
gridToPanelMap: any;
panelMap: { [id: string]: PanelModel };
panelRef: { [id: string]: HTMLElement } = {};

componentDidMount() {
const { dashboard } = this.props;
Expand Down Expand Up @@ -149,6 +150,9 @@ export class DashboardGrid extends PureComponent<Props> {
}

this.props.dashboard.sortPanelsByGridPos();

// Call render() after any changes. This is called when the layour loads
this.forceUpdate();
};

triggerForceUpdate = () => {
Expand All @@ -174,7 +178,6 @@ export class DashboardGrid extends PureComponent<Props> {
};

onResize: ItemCallback = (layout, oldItem, newItem) => {
console.log();
this.panelMap[newItem.i].updateGridPos(newItem);
};

Expand All @@ -187,18 +190,64 @@ export class DashboardGrid extends PureComponent<Props> {
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() {
const panelElements = [];

for (const panel of this.props.dashboard.panels) {
const panelClasses = classNames({ 'react-grid-item--fullscreen': panel.fullscreen });
const id = panel.id.toString();
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
panel={panel}
dashboard={this.props.dashboard}
isEditing={panel.isEditing}
isFullscreen={panel.fullscreen}
isInView={this.isInView(panel)}
/>
</div>
);
Expand Down
19 changes: 16 additions & 3 deletions public/app/features/dashboard/dashgrid/DashboardPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ export interface Props {
dashboard: DashboardModel;
isEditing: boolean;
isFullscreen: boolean;
isInView: boolean;
}

export interface State {
plugin: PanelPlugin;
angularPanel: AngularComponent;
isLazy: boolean;
}

export class DashboardPanel extends PureComponent<Props, State> {
Expand All @@ -40,6 +42,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
this.state = {
plugin: null,
angularPanel: null,
isLazy: !props.isInView,
};

this.specialPanels['row'] = this.renderRow.bind(this);
Expand Down Expand Up @@ -90,7 +93,11 @@ export class DashboardPanel extends PureComponent<Props, State> {
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) {
return;
}
Expand Down Expand Up @@ -123,7 +130,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
};

renderReactPanel() {
const { dashboard, panel, isFullscreen } = this.props;
const { dashboard, panel, isFullscreen, isInView } = this.props;
const { plugin } = this.state;

return (
Expand All @@ -138,6 +145,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
panel={panel}
dashboard={dashboard}
isFullscreen={isFullscreen}
isInView={isInView}
width={width}
height={height}
/>
Expand All @@ -153,7 +161,7 @@ export class DashboardPanel extends PureComponent<Props, State> {

render() {
const { panel, dashboard, isFullscreen, isEditing } = this.props;
const { plugin, angularPanel } = this.state;
const { plugin, angularPanel, isLazy } = this.state;

if (this.isSpecial(panel.type)) {
return this.specialPanels[panel.type]();
Expand All @@ -164,6 +172,11 @@ export class DashboardPanel extends PureComponent<Props, State> {
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 panelWrapperClass = classNames({
'panel-wrapper': true,
Expand Down
67 changes: 42 additions & 25 deletions public/app/features/dashboard/dashgrid/PanelChrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface Props {
dashboard: DashboardModel;
plugin: PanelPlugin;
isFullscreen: boolean;
isInView: boolean;
width: number;
height: number;
}
Expand All @@ -39,6 +40,7 @@ export interface State {
isFirstLoad: boolean;
renderCounter: number;
errorMessage: string | null;
refreshWhenInView: boolean;

// Current state of all events
data: PanelData;
Expand All @@ -47,14 +49,14 @@ export interface State {
export class PanelChrome extends PureComponent<Props, State> {
timeSrv: TimeSrv = getTimeSrv();
querySubscription: Unsubscribable;
delayedStateUpdate: Partial<State>;

constructor(props: Props) {
super(props);
this.state = {
isFirstLoad: true,
renderCounter: 0,
errorMessage: null,
refreshWhenInView: false,
data: {
state: LoadingState.NotStarted,
series: [],
Expand Down Expand Up @@ -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
// 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
panelDataObserver = {
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;

if (data.state === LoadingState.Error) {
const { error } = data;
if (error) {
if (this.state.errorMessage !== error.message) {
if (errorMessage !== error.message) {
errorMessage = error.message;
}
}
Expand All @@ -113,30 +144,26 @@ export class PanelChrome extends PureComponent<Props, State> {
if (this.props.dashboard.snapshot) {
this.props.panel.snapshotData = data.series;
}
if (this.state.isFirstLoad) {
if (isFirstLoad) {
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 = () => {
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;
}

const { panel, width } = this.props;
const timeData = applyPanelTimeOverrides(panel, this.timeSrv.timeRange());

// Issue Query
Expand Down Expand Up @@ -172,12 +199,6 @@ export class PanelChrome extends PureComponent<Props, State> {
onRender = () => {
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);
};

Expand All @@ -199,10 +220,6 @@ export class PanelChrome extends PureComponent<Props, State> {
}
};

get isVisible() {
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
}

get hasPanelSnapshot() {
const { panel } = this.props;
return panel.snapshotData && panel.snapshotData.length;
Expand Down

0 comments on commit c3a5204

Please sign in to comment.