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
This commit is contained in:
committed by
Torkel Ödegaard
parent
846b9327a5
commit
c3a5204933
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
}
|
||||
isEditing={false}
|
||||
isFullscreen={false}
|
||||
scrollTop={0}
|
||||
/>
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
@@ -540,6 +541,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
}
|
||||
isEditing={false}
|
||||
isFullscreen={false}
|
||||
scrollTop={0}
|
||||
/>
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
|
||||
@@ -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;
|
||||
@@ -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 = () => {
|
||||
@@ -174,7 +178,6 @@ export class DashboardGrid extends PureComponent<Props> {
|
||||
};
|
||||
|
||||
onResize: ItemCallback = (layout, oldItem, newItem) => {
|
||||
console.log();
|
||||
this.panelMap[newItem.i].updateGridPos(newItem);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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> {
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
@@ -138,6 +145,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
panel={panel}
|
||||
dashboard={dashboard}
|
||||
isFullscreen={isFullscreen}
|
||||
isInView={isInView}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
@@ -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]();
|
||||
@@ -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,
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface Props {
|
||||
dashboard: DashboardModel;
|
||||
plugin: PanelPlugin;
|
||||
isFullscreen: boolean;
|
||||
isInView: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
@@ -39,6 +40,7 @@ export interface State {
|
||||
isFirstLoad: boolean;
|
||||
renderCounter: number;
|
||||
errorMessage: string | null;
|
||||
refreshWhenInView: boolean;
|
||||
|
||||
// Current state of all events
|
||||
data: PanelData;
|
||||
@@ -47,7 +49,6 @@ export interface State {
|
||||
export class PanelChrome extends PureComponent<Props, State> {
|
||||
timeSrv: TimeSrv = getTimeSrv();
|
||||
querySubscription: Unsubscribable;
|
||||
delayedStateUpdate: Partial<State>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -55,6 +56,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
isFirstLoad: true,
|
||||
renderCounter: 0,
|
||||
errorMessage: null,
|
||||
refreshWhenInView: false,
|
||||
data: {
|
||||
state: LoadingState.NotStarted,
|
||||
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
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user