Lazy load metric & mardown visualizations (#78391) (#78563)

* Lazy load metrics vis

* Use common chart spinner

* Simplify markdown renderer

* Update tests

* Update types for metric vis

* Fix tests

* Fix merge conflict

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Daniil Suleiman 2020-09-28 14:24:45 +03:00 committed by GitHub
parent ca86c890b2
commit 4c3068cc7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 132 additions and 199 deletions

View file

@ -4,5 +4,5 @@
"ui": true,
"server": true,
"requiredPlugins": ["expressions", "visualizations"],
"requiredBundles": ["kibanaUtils", "kibanaReact", "charts", "visualizations", "expressions", "visDefaultEditor"]
"requiredBundles": ["kibanaReact", "charts", "visualizations", "expressions", "visDefaultEditor"]
}

View file

@ -5,7 +5,7 @@ Object {
"as": "markdown_vis",
"type": "render",
"value": Object {
"visConfig": Object {
"visParams": Object {
"fontSize": 12,
"markdown": "## hello _markdown_",
"openLinksInNewTab": true,

View file

@ -1,8 +0,0 @@
// Prefix all styles with "mkd" to avoid conflicts.
// Examples
// mkdChart
// mkdChart__legend
// mkdChart__legend--small
// mkdChart__legend-isLoading
@import './markdown_vis';

View file

@ -21,16 +21,16 @@ import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, Render } from '../../expressions/public';
import { Arguments, MarkdownVisParams } from './types';
interface RenderValue {
export interface MarkdownVisRenderValue {
visType: 'markdown';
visConfig: MarkdownVisParams;
visParams: MarkdownVisParams;
}
export type MarkdownVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
'markdownVis',
unknown,
Arguments,
Render<RenderValue>
Render<MarkdownVisRenderValue>
>;
export const createMarkdownVisFn = (): MarkdownVisExpressionFunctionDefinition => ({
@ -70,7 +70,7 @@ export const createMarkdownVisFn = (): MarkdownVisExpressionFunctionDefinition =
as: 'markdown_vis',
value: {
visType: 'markdown',
visConfig: {
visParams: {
markdown: args.markdown,
openLinksInNewTab: args.openLinksInNewTab,
fontSize: parseInt(args.font.spec.fontSize || '12', 10),

View file

@ -17,41 +17,29 @@
* under the License.
*/
import React from 'react';
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { VisualizationContainer } from '../../visualizations/public';
import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers';
import { MarkdownVisWrapper } from './markdown_vis_controller';
import { StartServicesGetter } from '../../kibana_utils/public';
import { MarkdownVisRenderValue } from './markdown_fn';
export const getMarkdownRenderer = (start: StartServicesGetter) => {
const markdownVisRenderer: () => ExpressionRenderDefinition = () => ({
name: 'markdown_vis',
displayName: 'markdown visualization',
reuseDomNode: true,
render: async (domNode: HTMLElement, config: any, handlers: any) => {
const { visConfig } = config;
// @ts-ignore
const MarkdownVisComponent = lazy(() => import('./markdown_vis_controller'));
const I18nContext = await start().core.i18n.Context;
export const markdownVisRenderer: ExpressionRenderDefinition<MarkdownVisRenderValue> = {
name: 'markdown_vis',
displayName: 'markdown visualization',
reuseDomNode: true,
render: async (domNode, { visParams }, handlers) => {
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
render(
<I18nContext>
<VisualizationContainer className="markdownVis">
<MarkdownVisWrapper
visParams={visConfig}
renderComplete={handlers.done}
fireEvent={handlers.event}
/>
</VisualizationContainer>
</I18nContext>,
domNode
);
},
});
return markdownVisRenderer;
render(
<VisualizationContainer className="markdownVis">
<MarkdownVisComponent {...visParams} renderComplete={handlers.done} />
</VisualizationContainer>,
domNode
);
},
};

View file

@ -1,3 +1,10 @@
// Prefix all styles with "mkd" to avoid conflicts.
// Examples
// mkdChart
// mkdChart__legend
// mkdChart__legend--small
// mkdChart__legend-isLoading
.mkdVis {
padding: $euiSizeS;
width: 100%;

View file

@ -20,7 +20,7 @@
import React from 'react';
import { wait } from '@testing-library/dom';
import { render, cleanup } from '@testing-library/react/pure';
import { MarkdownVisWrapper } from './markdown_vis_controller';
import MarkdownVisComponent from './markdown_vis_controller';
afterEach(cleanup);
@ -36,7 +36,7 @@ describe('markdown vis controller', () => {
};
const { getByTestId, getByText } = render(
<MarkdownVisWrapper visParams={vis.params} renderComplete={jest.fn()} fireEvent={jest.fn()} />
<MarkdownVisComponent {...vis.params} renderComplete={jest.fn()} />
);
await wait(() => getByTestId('markdownBody'));
@ -60,7 +60,7 @@ describe('markdown vis controller', () => {
};
const { getByTestId, getByText } = render(
<MarkdownVisWrapper visParams={vis.params} renderComplete={jest.fn()} fireEvent={jest.fn()} />
<MarkdownVisComponent {...vis.params} renderComplete={jest.fn()} />
);
await wait(() => getByTestId('markdownBody'));
@ -82,7 +82,7 @@ describe('markdown vis controller', () => {
};
const { getByTestId, getByText, rerender } = render(
<MarkdownVisWrapper visParams={vis.params} renderComplete={jest.fn()} fireEvent={jest.fn()} />
<MarkdownVisComponent {...vis.params} renderComplete={jest.fn()} />
);
await wait(() => getByTestId('markdownBody'));
@ -90,9 +90,7 @@ describe('markdown vis controller', () => {
expect(getByText(/initial/i)).toBeInTheDocument();
vis.params.markdown = 'Updated';
rerender(
<MarkdownVisWrapper visParams={vis.params} renderComplete={jest.fn()} fireEvent={jest.fn()} />
);
rerender(<MarkdownVisComponent {...vis.params} renderComplete={jest.fn()} />);
expect(getByText(/Updated/i)).toBeInTheDocument();
});
@ -114,11 +112,7 @@ describe('markdown vis controller', () => {
it('should be called on initial rendering', async () => {
const { getByTestId } = render(
<MarkdownVisWrapper
visParams={vis.params}
renderComplete={renderComplete}
fireEvent={jest.fn()}
/>
<MarkdownVisComponent {...vis.params} renderComplete={renderComplete} />
);
await wait(() => getByTestId('markdownBody'));
@ -128,11 +122,7 @@ describe('markdown vis controller', () => {
it('should be called on successive render when params change', async () => {
const { getByTestId, rerender } = render(
<MarkdownVisWrapper
visParams={vis.params}
renderComplete={renderComplete}
fireEvent={jest.fn()}
/>
<MarkdownVisComponent {...vis.params} renderComplete={renderComplete} />
);
await wait(() => getByTestId('markdownBody'));
@ -142,24 +132,14 @@ describe('markdown vis controller', () => {
renderComplete.mockClear();
vis.params.markdown = 'changed';
rerender(
<MarkdownVisWrapper
visParams={vis.params}
renderComplete={renderComplete}
fireEvent={jest.fn()}
/>
);
rerender(<MarkdownVisComponent {...vis.params} renderComplete={renderComplete} />);
expect(renderComplete).toHaveBeenCalledTimes(1);
});
it('should be called on successive render even without data change', async () => {
const { getByTestId, rerender } = render(
<MarkdownVisWrapper
visParams={vis.params}
renderComplete={renderComplete}
fireEvent={jest.fn()}
/>
<MarkdownVisComponent {...vis.params} renderComplete={renderComplete} />
);
await wait(() => getByTestId('markdownBody'));
@ -168,13 +148,7 @@ describe('markdown vis controller', () => {
renderComplete.mockClear();
rerender(
<MarkdownVisWrapper
visParams={vis.params}
renderComplete={renderComplete}
fireEvent={jest.fn()}
/>
);
rerender(<MarkdownVisComponent {...vis.params} renderComplete={renderComplete} />);
expect(renderComplete).toHaveBeenCalledTimes(1);
});

View file

@ -17,83 +17,35 @@
* under the License.
*/
import React from 'react';
import React, { useEffect } from 'react';
import { Markdown } from '../../kibana_react/public';
import { MarkdownVisParams } from './types';
import './markdown_vis.scss';
interface MarkdownVisComponentProps extends MarkdownVisParams {
renderComplete: () => void;
}
/**
* The MarkdownVisComponent renders markdown to HTML and presents it.
*/
class MarkdownVisComponent extends React.Component<MarkdownVisComponentProps> {
/**
* Will be called after the first render when the component is present in the DOM.
*
* We call renderComplete here, to signal, that we are done with rendering.
*/
componentDidMount() {
this.props.renderComplete();
}
const MarkdownVisComponent = ({
fontSize,
markdown,
openLinksInNewTab,
renderComplete,
}: MarkdownVisComponentProps) => {
useEffect(renderComplete); // renderComplete will be called after each render to signal, that we are done with rendering.
/**
* Will be called after the component has been updated and the changes has been
* flushed into the DOM.
*
* We will use this to signal that we are done rendering by calling the
* renderComplete property.
*/
componentDidUpdate() {
this.props.renderComplete();
}
/**
* Render the actual HTML.
* Note: if only fontSize parameter has changed, this method will be called
* and return the appropriate JSX, but React will detect, that only the
* style argument has been updated, and thus only set this attribute to the DOM.
*/
render() {
return (
<div className="mkdVis" style={{ fontSize: `${this.props.fontSize}pt` }}>
<Markdown
data-test-subj="markdownBody"
markdown={this.props.markdown}
openLinksInNewTab={this.props.openLinksInNewTab}
/>
</div>
);
}
}
/**
* This is a wrapper component, that is actually used as the visualization.
* The sole purpose of this component is to extract all required parameters from
* the properties and pass them down as separate properties to the actual component.
* That way the actual (MarkdownVisComponent) will properly trigger it's prop update
* callback (componentWillReceiveProps) if one of these params change. It wouldn't
* trigger otherwise (e.g. it doesn't for this wrapper), since it only triggers
* if the reference to the prop changes (in this case the reference to vis).
*
* The way React works, this wrapper nearly brings no overhead, but allows us
* to use proper lifecycle methods in the actual component.
*/
export interface MarkdownVisWrapperProps {
visParams: MarkdownVisParams;
fireEvent: (event: any) => void;
renderComplete: () => void;
}
export function MarkdownVisWrapper(props: MarkdownVisWrapperProps) {
return (
<MarkdownVisComponent
fontSize={props.visParams.fontSize}
markdown={props.visParams.markdown}
openLinksInNewTab={props.visParams.openLinksInNewTab}
renderComplete={props.renderComplete}
/>
<div className="mkdVis" style={{ fontSize: `${fontSize}pt` }}>
<Markdown
data-test-subj="markdownBody"
markdown={markdown}
openLinksInNewTab={openLinksInNewTab}
/>
</div>
);
}
};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { MarkdownVisComponent as default };

View file

@ -24,10 +24,7 @@ import { VisualizationsSetup } from '../../visualizations/public';
import { markdownVisDefinition } from './markdown_vis';
import { createMarkdownVisFn } from './markdown_fn';
import { ConfigSchema } from '../config';
import './index.scss';
import { getMarkdownRenderer } from './markdown_renderer';
import { createStartServicesGetter } from '../../kibana_utils/public';
import { markdownVisRenderer } from './markdown_renderer';
/** @internal */
export interface MarkdownPluginSetupDependencies {
@ -44,9 +41,8 @@ export class MarkdownPlugin implements Plugin<void, void> {
}
public setup(core: CoreSetup, { expressions, visualizations }: MarkdownPluginSetupDependencies) {
const start = createStartServicesGetter(core.getStartServices);
visualizations.createBaseVisualization(markdownVisDefinition);
expressions.registerRenderer(getMarkdownRenderer(start));
expressions.registerRenderer(markdownVisRenderer);
expressions.registerFunction(createMarkdownVisFn);
}

View file

@ -1,3 +1,10 @@
// Prefix all styles with "mtr" to avoid conflicts.
// Examples
// mtrChart
// mtrChart__legend
// mtrChart__legend--small
// mtrChart__legend-isLoading
.mtrVis {
width: 100%;
display: flex;

View file

@ -20,7 +20,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { MetricVisComponent, MetricVisComponentProps } from './metric_vis_component';
import MetricVisComponent, { MetricVisComponentProps } from './metric_vis_component';
jest.mock('../services', () => ({
getFormatService: () => ({

View file

@ -30,14 +30,16 @@ import { getFormatService } from '../services';
import { SchemaConfig } from '../../../visualizations/public';
import { Range } from '../../../expressions/public';
import './metric_vis.scss';
export interface MetricVisComponentProps {
visParams: VisParams;
visParams: Pick<VisParams, 'metric' | 'dimensions'>;
visData: Input;
fireEvent: (event: any) => void;
renderComplete: () => void;
}
export class MetricVisComponent extends Component<MetricVisComponentProps> {
class MetricVisComponent extends Component<MetricVisComponentProps> {
private getLabels() {
const config = this.props.visParams.metric;
const isPercentageMode = config.percentageMode;
@ -209,3 +211,7 @@ export class MetricVisComponent extends Component<MetricVisComponentProps> {
return metricsHtml;
}
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { MetricVisComponent as default };

View file

@ -1,8 +0,0 @@
// Prefix all styles with "mtr" to avoid conflicts.
// Examples
// mtrChart
// mtrChart__legend
// mtrChart__legend--small
// mtrChart__legend-isLoading
@import 'metric_vis';

View file

@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import './index.scss';
import { PluginInitializerContext } from 'kibana/public';
import { MetricVisPlugin as Plugin } from './plugin';

View file

@ -46,7 +46,7 @@ interface Arguments {
bucket: any; // these aren't typed yet
}
interface RenderValue {
export interface MetricVisRenderValue {
visType: typeof visType;
visData: Input;
visConfig: Pick<VisParams, 'metric' | 'dimensions'>;
@ -57,7 +57,7 @@ export type MetricVisExpressionFunctionDefinition = ExpressionFunctionDefinition
'metricVis',
Input,
Arguments,
Render<RenderValue>
Render<MetricVisRenderValue>
>;
export const createMetricVisFn = (): MetricVisExpressionFunctionDefinition => ({

View file

@ -17,37 +17,33 @@
* under the License.
*/
import React from 'react';
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { MetricVisComponent } from './components/metric_vis_component';
import { getI18n } from './services';
import { VisualizationContainer } from '../../visualizations/public';
import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers';
import { MetricVisRenderValue } from './metric_vis_fn';
// @ts-ignore
const MetricVisComponent = lazy(() => import('./components/metric_vis_component'));
export const metricVisRenderer: () => ExpressionRenderDefinition = () => ({
export const metricVisRenderer: () => ExpressionRenderDefinition<MetricVisRenderValue> = () => ({
name: 'metric_vis',
displayName: 'metric visualization',
reuseDomNode: true,
render: async (domNode: HTMLElement, config: any, handlers: any) => {
const { visData, visConfig } = config;
const I18nContext = getI18n().Context;
render: async (domNode, { visData, visConfig }, handlers) => {
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
render(
<I18nContext>
<VisualizationContainer className="mtrVis">
<MetricVisComponent
visData={visData}
visParams={visConfig}
renderComplete={handlers.done}
fireEvent={handlers.event}
/>
</VisualizationContainer>
</I18nContext>,
<VisualizationContainer className="mtrVis" showNoResult={!visData.rows?.length}>
<MetricVisComponent
visData={visData}
visParams={visConfig}
renderComplete={handlers.done}
fireEvent={handlers.event}
/>
</VisualizationContainer>,
domNode
);
},

View file

@ -25,7 +25,7 @@ import { createMetricVisFn } from './metric_vis_fn';
import { createMetricVisTypeDefinition } from './metric_vis_type';
import { ChartsPluginSetup } from '../../charts/public';
import { DataPublicPluginStart } from '../../data/public';
import { setFormatService, setI18n } from './services';
import { setFormatService } from './services';
import { ConfigSchema } from '../config';
import { metricVisRenderer } from './metric_vis_renderer';
@ -59,7 +59,6 @@ export class MetricVisPlugin implements Plugin<void, void> {
}
public start(core: CoreStart, { data }: MetricVisPluginStartDependencies) {
setI18n(core.i18n);
setFormatService(data.fieldFormats);
}
}

View file

@ -17,12 +17,9 @@
* under the License.
*/
import { I18nStart } from 'kibana/public';
import { createGetterSetter } from '../../kibana_utils/common';
import { DataPublicPluginStart } from '../../data/public';
export const [getFormatService, setFormatService] = createGetterSetter<
DataPublicPluginStart['fieldFormats']
>('metric data.fieldFormats');
export const [getI18n, setI18n] = createGetterSetter<I18nStart>('I18n');

View file

@ -70,3 +70,10 @@
flex-direction: column;
}
.visChart__spinner {
display: flex;
flex: 1 1 auto;
justify-content: center;
align-items: center;
}

View file

@ -17,14 +17,35 @@
* under the License.
*/
import React, { ReactNode } from 'react';
import React, { ReactNode, Suspense } from 'react';
import { EuiLoadingChart } from '@elastic/eui';
import classNames from 'classnames';
import { VisualizationNoResults } from './visualization_noresults';
interface VisualizationContainerProps {
className?: string;
children: ReactNode;
showNoResult?: boolean;
}
export const VisualizationContainer = (props: VisualizationContainerProps) => {
const classes = `visualization ${props.className}`;
return <div className={classes}>{props.children}</div>;
export const VisualizationContainer = ({
className,
children,
showNoResult = false,
}: VisualizationContainerProps) => {
const classes = classNames('visualization', className);
const fallBack = (
<div className="visChart__spinner">
<EuiLoadingChart mono size="l" />
</div>
);
return (
<div className={classes}>
<Suspense fallback={fallBack}>
{showNoResult ? <VisualizationNoResults /> : children}
</Suspense>
</div>
);
};