Skip to content

Commit

Permalink
[dashboard][labs] Defer loading panels below the fold
Browse files Browse the repository at this point in the history
  • Loading branch information
clintandrewhall committed May 12, 2021
1 parent ae08154 commit 2d8efd7
Show file tree
Hide file tree
Showing 17 changed files with 294 additions and 74 deletions.
4 changes: 4 additions & 0 deletions src/plugins/dashboard/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ export {
} from './types';

export { migratePanelsTo730 } from './migrate_to_730_panels';

export const UI_SETTINGS = {
ENABLE_LABS_UI: 'labs:dashboard:enable_ui',
};
36 changes: 20 additions & 16 deletions src/plugins/dashboard/public/application/dashboard_router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {

import { createKbnUrlStateStorage, withNotifyOnErrors } from '../services/kibana_utils';
import { KibanaContextProvider } from '../services/kibana_react';

import {
AppMountParameters,
CoreSetup,
Expand Down Expand Up @@ -81,6 +82,7 @@ export async function mountApp({
kibanaLegacy: { dashboardConfig },
savedObjectsTaggingOss,
visualizations,
presentationUtil,
} = pluginsStart;

const spacesApi = pluginsStart.spacesOss?.isSpacesAvailable ? pluginsStart.spacesOss : undefined;
Expand Down Expand Up @@ -208,22 +210,24 @@ export async function mountApp({
const app = (
<I18nProvider>
<KibanaContextProvider services={dashboardServices}>
<HashRouter>
<Switch>
<Route
path={[
DashboardConstants.CREATE_NEW_DASHBOARD_URL,
`${DashboardConstants.VIEW_DASHBOARD_URL}/:id`,
]}
render={renderDashboard}
/>
<Route exact path={DashboardConstants.LANDING_PAGE_PATH} render={renderListingPage} />
<Route exact path="/">
<Redirect to={DashboardConstants.LANDING_PAGE_PATH} />
</Route>
<Route render={renderNoMatch} />
</Switch>
</HashRouter>
<presentationUtil.ContextProvider>
<HashRouter>
<Switch>
<Route
path={[
DashboardConstants.CREATE_NEW_DASHBOARD_URL,
`${DashboardConstants.VIEW_DASHBOARD_URL}/:id`,
]}
render={renderDashboard}
/>
<Route exact path={DashboardConstants.LANDING_PAGE_PATH} render={renderListingPage} />
<Route exact path="/">
<Redirect to={DashboardConstants.LANDING_PAGE_PATH} />
</Route>
<Route render={renderNoMatch} />
</Switch>
</HashRouter>
</presentationUtil.ContextProvider>
</KibanaContextProvider>
</I18nProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import { PLACEHOLDER_EMBEDDABLE } from './placeholder';
import { PanelPlacementMethod, IPanelPlacementArgs } from './panel/dashboard_panel_placement';
import { DashboardCapabilities } from '../types';
import { PresentationUtilPluginStart } from '../../services/presentation_util';

export interface DashboardContainerInput extends ContainerInput {
dashboardCapabilities?: DashboardCapabilities;
Expand Down Expand Up @@ -68,6 +69,7 @@ export interface DashboardContainerServices {
embeddable: EmbeddableStart;
uiActions: UiActionsStart;
http: CoreStart['http'];
presentationUtil: PresentationUtilPluginStart;
}

interface IndexSignature {
Expand Down Expand Up @@ -245,7 +247,9 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
ReactDOM.render(
<I18nProvider>
<KibanaContextProvider services={this.services}>
<DashboardViewport container={this} switchViewMode={this.switchViewMode} />
<this.services.presentationUtil.ContextProvider>
<DashboardViewport container={this} switchViewMode={this.switchViewMode} />
</this.services.presentationUtil.ContextProvider>
</KibanaContextProvider>
</I18nProvider>,
dom
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import React from 'react';
import { Subscription } from 'rxjs';
import ReactGridLayout, { Layout } from 'react-grid-layout';
import { GridData } from '../../../../common';
import { ViewMode, EmbeddableChildPanel } from '../../../services/embeddable';
import { ViewMode } from '../../../services/embeddable';
import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants';
import { DashboardPanelState } from '../types';
import { withKibana } from '../../../services/kibana_react';
import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container';
import { DashboardGridItem } from './dashboard_grid_item';

let lastValidGridSize = 0;

Expand Down Expand Up @@ -123,9 +124,6 @@ interface PanelLayout extends Layout {
class DashboardGridUi extends React.Component<DashboardGridProps, State> {
private subscription?: Subscription;
private mounted: boolean = false;
// A mapping of panelIndexes to grid items so we can set the zIndex appropriately on the last focused
// item.
private gridItems = {} as { [key: string]: HTMLDivElement | null };

constructor(props: DashboardGridProps) {
super(props);
Expand Down Expand Up @@ -222,13 +220,20 @@ class DashboardGridUi extends React.Component<DashboardGridProps, State> {
}
};

public renderPanels() {
const { focusedPanelIndex, panels, expandedPanelId } = this.state;
public render() {
if (this.state.isLayoutInvalid) {
return null;
}

const { container, kibana } = this.props;
const { focusedPanelIndex, panels, expandedPanelId, viewMode } = this.state;
const isViewMode = viewMode === ViewMode.VIEW;

// Part of our unofficial API - need to render in a consistent order for plugins.
const panelsInOrder = Object.keys(panels).map(
(key: string) => panels[key] as DashboardPanelState
);

panelsInOrder.sort((panelA, panelB) => {
if (panelA.gridData.y === panelB.gridData.y) {
return panelA.gridData.x - panelB.gridData.x;
Expand All @@ -237,55 +242,27 @@ class DashboardGridUi extends React.Component<DashboardGridProps, State> {
}
});

return _.map(panelsInOrder, (panel) => {
const expandPanel =
expandedPanelId !== undefined && expandedPanelId === panel.explicitInput.id;
const hidePanel = expandedPanelId !== undefined && expandedPanelId !== panel.explicitInput.id;
const classes = classNames({
// eslint-disable-next-line @typescript-eslint/naming-convention
'dshDashboardGrid__item--expanded': expandPanel,
// eslint-disable-next-line @typescript-eslint/naming-convention
'dshDashboardGrid__item--hidden': hidePanel,
});
return (
<div
style={{ zIndex: focusedPanelIndex === panel.explicitInput.id ? 2 : 'auto' }}
className={classes}
// This key is required for the ReactGridLayout to work properly
key={panel.explicitInput.id}
data-test-subj="dashboardPanel"
ref={(reactGridItem) => {
this.gridItems[panel.explicitInput.id] = reactGridItem;
}}
>
<EmbeddableChildPanel
// This key is used to force rerendering on embeddable type change while the id remains the same
key={panel.type}
embeddableId={panel.explicitInput.id}
container={this.props.container}
PanelComponent={this.props.kibana.services.embeddable.EmbeddablePanel}
/>
</div>
);
});
}

public render() {
if (this.state.isLayoutInvalid) {
return null;
}
const dashboardPanels = _.map(panelsInOrder, ({ explicitInput, type }) => (
<DashboardGridItem
id={explicitInput.id}
key={explicitInput.id}
type={type}
container={container}
PanelComponent={kibana.services.embeddable.EmbeddablePanel}
expandedPanelId={expandedPanelId}
focusedPanelId={focusedPanelIndex}
/>
));

const { viewMode } = this.state;
const isViewMode = viewMode === ViewMode.VIEW;
return (
<ResponsiveSizedGrid
isViewMode={isViewMode}
layout={this.buildLayoutFromPanels()}
onLayoutChange={this.onLayoutChange}
maximizedPanelId={this.state.expandedPanelId}
maximizedPanelId={expandedPanelId}
useMargins={this.state.useMargins}
>
{this.renderPanels()}
{dashboardPanels}
</ResponsiveSizedGrid>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React, { useState, useRef, useEffect, FC } from 'react';
import { EuiLoadingChart } from '@elastic/eui';
import classNames from 'classnames';

import { EmbeddableChildPanel } from '../../../services/embeddable';
import { useLabs } from '../../../services/presentation_util';
import { DashboardPanelState } from '../types';

type PanelProps = Pick<EmbeddableChildPanel, 'container' | 'PanelComponent'>;
type DivProps = Pick<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'children'>;

interface Props extends PanelProps, DivProps {
id: DashboardPanelState['explicitInput']['id'];
type: DashboardPanelState['type'];
focusedPanelId?: string;
expandedPanelId?: string;
key: string;
isRenderable?: boolean;
}

const Item = React.forwardRef<HTMLDivElement, Props>(
(
{
container,
expandedPanelId,
focusedPanelId,
id,
PanelComponent,
type,
isRenderable = true,
// The props below are passed from ReactGridLayoutn and need to be merged with their counterparts.
// https://github.com/react-grid-layout/react-grid-layout/issues/1241#issuecomment-658306889
children,
className,
style,
...rest
},
ref
) => {
const expandPanel = expandedPanelId !== undefined && expandedPanelId === id;
const hidePanel = expandedPanelId !== undefined && expandedPanelId !== id;
const classes = classNames({
// eslint-disable-next-line @typescript-eslint/naming-convention
'dshDashboardGrid__item--expanded': expandPanel,
// eslint-disable-next-line @typescript-eslint/naming-convention
'dshDashboardGrid__item--hidden': hidePanel,
});

return (
<div
style={{ ...style, zIndex: focusedPanelId === id ? 2 : 'auto' }}
className={[classes, className].join(' ')}
data-test-subj="dashboardPanel"
ref={ref}
{...rest}
>
{isRenderable ? (
<>
<EmbeddableChildPanel
// This key is used to force rerendering on embeddable type change while the id remains the same
key={type}
embeddableId={id}
{...{ container, PanelComponent }}
/>
{children}
</>
) : (
<div className="embPanel embPanel-isLoading">
<EuiLoadingChart size="l" mono />
</div>
)}
</div>
);
}
);

export const ObservedItem: FC<Props> = (props: Props) => {
const [intersection, updateIntersection] = useState<IntersectionObserverEntry>();
const [isRenderable, setIsRenderable] = useState(false);
const panelRef = useRef<HTMLDivElement>(null);

const observerRef = useRef(
new window.IntersectionObserver(([value]) => updateIntersection(value), {
root: panelRef.current,
})
);

useEffect(() => {
const { current: currentObserver } = observerRef;
currentObserver.disconnect();
const { current } = panelRef;

if (current) {
currentObserver.observe(current);
}

return () => currentObserver.disconnect();
}, [panelRef]);

useEffect(() => {
if (intersection?.isIntersecting && !isRenderable) {
setIsRenderable(true);
}
}, [intersection, isRenderable]);

return <Item ref={panelRef} isRenderable={isRenderable} {...props} />;
};

export const DashboardGridItem: FC<Props> = (props: Props) => {
const { isProjectEnabled } = useLabs();
const isEnabled = isProjectEnabled('labs:dashboard:deferBelowFold');

return isEnabled ? <ObservedItem {...props} /> : <Item {...props} />;
};
Loading

0 comments on commit 2d8efd7

Please sign in to comment.