[Lens] Make Lens compatible with reporting (#46027)
This commit is contained in:
parent
5777a02501
commit
b0c3ea042c
|
@ -13,6 +13,7 @@ import { KibanaDatatable } from '../../../../../../src/legacy/core_plugins/inter
|
|||
import { LensMultiTable } from '../types';
|
||||
import { IInterpreterRenderFunction } from '../../../../../../src/legacy/core_plugins/expressions/public';
|
||||
import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities';
|
||||
import { VisualizationContainer } from '../visualization_container';
|
||||
|
||||
export interface DatatableColumns {
|
||||
columnIds: string[];
|
||||
|
@ -132,28 +133,30 @@ function DatatableComponent(props: DatatableProps & { formatFactory: FormatFacto
|
|||
});
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
className="lnsDataTable"
|
||||
data-test-subj="lnsDataTable"
|
||||
columns={props.args.columns.columnIds
|
||||
.map((field, index) => {
|
||||
return {
|
||||
field,
|
||||
name: props.args.columns.labels[index],
|
||||
};
|
||||
})
|
||||
.filter(({ field }) => !!field)}
|
||||
items={
|
||||
firstTable
|
||||
? firstTable.rows.map(row => {
|
||||
const formattedRow: Record<string, unknown> = {};
|
||||
Object.entries(formatters).forEach(([columnId, formatter]) => {
|
||||
formattedRow[columnId] = formatter.convert(row[columnId]);
|
||||
});
|
||||
return formattedRow;
|
||||
})
|
||||
: []
|
||||
}
|
||||
/>
|
||||
<VisualizationContainer>
|
||||
<EuiBasicTable
|
||||
className="lnsDataTable"
|
||||
data-test-subj="lnsDataTable"
|
||||
columns={props.args.columns.columnIds
|
||||
.map((field, index) => {
|
||||
return {
|
||||
field,
|
||||
name: props.args.columns.labels[index],
|
||||
};
|
||||
})
|
||||
.filter(({ field }) => !!field)}
|
||||
items={
|
||||
firstTable
|
||||
? firstTable.rows.map(row => {
|
||||
const formattedRow: Record<string, unknown> = {};
|
||||
Object.entries(formatters).forEach(([columnId, formatter]) => {
|
||||
formattedRow[columnId] = formatter.convert(row[columnId]);
|
||||
});
|
||||
return formattedRow;
|
||||
})
|
||||
: []
|
||||
}
|
||||
/>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -52,7 +52,8 @@ describe('metric_expression', () => {
|
|||
|
||||
expect(shallow(<MetricChart data={data} args={args} formatFactory={x => x as FieldFormat} />))
|
||||
.toMatchInlineSnapshot(`
|
||||
<div
|
||||
<VisualizationContainer
|
||||
reportTitle="My fanci metric chart"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
|
@ -88,7 +89,7 @@ describe('metric_expression', () => {
|
|||
My fanci metric chart
|
||||
</div>
|
||||
</AutoScale>
|
||||
</div>
|
||||
</VisualizationContainer>
|
||||
`);
|
||||
});
|
||||
|
||||
|
@ -104,34 +105,35 @@ describe('metric_expression', () => {
|
|||
/>
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
"maxHeight": "100%",
|
||||
"maxWidth": "100%",
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<AutoScale>
|
||||
<div
|
||||
data-test-subj="lns_metric_value"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "60pt",
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
10110
|
||||
</div>
|
||||
</AutoScale>
|
||||
</div>
|
||||
`);
|
||||
<VisualizationContainer
|
||||
reportTitle="My fanci metric chart"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
"maxHeight": "100%",
|
||||
"maxWidth": "100%",
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<AutoScale>
|
||||
<div
|
||||
data-test-subj="lns_metric_value"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "60pt",
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
10110
|
||||
</div>
|
||||
</AutoScale>
|
||||
</VisualizationContainer>
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import { IInterpreterRenderFunction } from '../../../../../../src/legacy/core_pl
|
|||
import { MetricConfig } from './types';
|
||||
import { LensMultiTable } from '../types';
|
||||
import { AutoScale } from './auto_scale';
|
||||
import { VisualizationContainer } from '../visualization_container';
|
||||
|
||||
export interface MetricChartProps {
|
||||
data: LensMultiTable;
|
||||
|
@ -105,7 +106,8 @@ export function MetricChart({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<VisualizationContainer
|
||||
reportTitle={title}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
@ -126,6 +128,6 @@ export function MetricChart({
|
|||
</div>
|
||||
)}
|
||||
</AutoScale>
|
||||
</div>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { VisualizationContainer } from './visualization_container';
|
||||
|
||||
describe('VisualizationContainer', () => {
|
||||
test('renders reporting data attributes when ready', () => {
|
||||
const component = mount(<VisualizationContainer isReady={true}>Hello!</VisualizationContainer>);
|
||||
|
||||
const reportingEl = component.find('[data-shared-item]').first();
|
||||
expect(reportingEl.prop('data-render-complete')).toBeTruthy();
|
||||
expect(reportingEl.prop('data-shared-item')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('does not render data attributes when not ready', () => {
|
||||
const component = mount(
|
||||
<VisualizationContainer isReady={false}>Hello!</VisualizationContainer>
|
||||
);
|
||||
|
||||
const reportingEl = component.find('[data-shared-item]').first();
|
||||
expect(reportingEl.prop('data-render-complete')).toBeFalsy();
|
||||
expect(reportingEl.prop('data-shared-item')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('renders child content', () => {
|
||||
const component = mount(
|
||||
<VisualizationContainer isReady={false}>Hello!</VisualizationContainer>
|
||||
);
|
||||
|
||||
expect(component.text()).toEqual('Hello!');
|
||||
});
|
||||
|
||||
test('defaults to rendered', () => {
|
||||
const component = mount(<VisualizationContainer>Hello!</VisualizationContainer>);
|
||||
const reportingEl = component.find('[data-shared-item]').first();
|
||||
|
||||
expect(reportingEl.prop('data-render-complete')).toBeTruthy();
|
||||
expect(reportingEl.prop('data-shared-item')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('renders title for reporting, if provided', () => {
|
||||
const component = mount(
|
||||
<VisualizationContainer reportTitle="shazam!">Hello!</VisualizationContainer>
|
||||
);
|
||||
const reportingEl = component.find('[data-shared-item]').first();
|
||||
|
||||
expect(reportingEl.prop('data-title')).toEqual('shazam!');
|
||||
});
|
||||
|
||||
test('renders style', () => {
|
||||
const component = mount(
|
||||
<VisualizationContainer style={{ color: 'blue' }}>Hello!</VisualizationContainer>
|
||||
);
|
||||
const reportingEl = component.find('[data-shared-item]').first();
|
||||
|
||||
expect(reportingEl.prop('style')).toEqual({ color: 'blue' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
isReady?: boolean;
|
||||
reportTitle?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a convenience component that wraps rendered Lens visualizations. It adds reporting
|
||||
* attributes (data-shared-item, data-render-complete, and data-title).
|
||||
*/
|
||||
export function VisualizationContainer({ isReady = true, reportTitle, children, ...rest }: Props) {
|
||||
return (
|
||||
<div data-shared-item data-render-complete={isReady} data-title={reportTitle} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
Chart,
|
||||
|
@ -26,6 +26,7 @@ import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/
|
|||
import { IInterpreterRenderFunction } from '../../../../../../src/legacy/core_plugins/expressions/public';
|
||||
import { LensMultiTable } from '../types';
|
||||
import { XYArgs, SeriesType, visualizationTypes } from './types';
|
||||
import { VisualizationContainer } from '../visualization_container';
|
||||
|
||||
export interface XYChartProps {
|
||||
data: LensMultiTable;
|
||||
|
@ -38,6 +39,16 @@ export interface XYRender {
|
|||
value: XYChartProps;
|
||||
}
|
||||
|
||||
export interface XYChartProps {
|
||||
data: LensMultiTable;
|
||||
args: XYArgs;
|
||||
}
|
||||
|
||||
type XYChartRenderProps = XYChartProps & {
|
||||
formatFactory: FormatFactory;
|
||||
timeZone: string;
|
||||
};
|
||||
|
||||
export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs, XYRender> = ({
|
||||
name: 'lens_xy_chart',
|
||||
type: 'render',
|
||||
|
@ -85,11 +96,6 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs
|
|||
// TODO the typings currently don't support custom type args. As soon as they do, this can be removed
|
||||
} as unknown) as ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs, XYRender>;
|
||||
|
||||
export interface XYChartProps {
|
||||
data: LensMultiTable;
|
||||
args: XYArgs;
|
||||
}
|
||||
|
||||
export const getXyChartRenderer = (dependencies: {
|
||||
formatFactory: FormatFactory;
|
||||
timeZone: string;
|
||||
|
@ -104,7 +110,7 @@ export const getXyChartRenderer = (dependencies: {
|
|||
render: async (domNode: Element, config: XYChartProps, _handlers: unknown) => {
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
<XYChart {...config} {...dependencies} />
|
||||
<XYChartReportable {...config} {...dependencies} />
|
||||
</I18nProvider>,
|
||||
domNode
|
||||
);
|
||||
|
@ -115,15 +121,25 @@ function getIconForSeriesType(seriesType: SeriesType): IconType {
|
|||
return visualizationTypes.find(c => c.id === seriesType)!.icon || 'empty';
|
||||
}
|
||||
|
||||
export function XYChart({
|
||||
data,
|
||||
args,
|
||||
formatFactory,
|
||||
timeZone,
|
||||
}: XYChartProps & {
|
||||
formatFactory: FormatFactory;
|
||||
timeZone: string;
|
||||
}) {
|
||||
const MemoizedChart = React.memo(XYChart);
|
||||
|
||||
export function XYChartReportable(props: XYChartRenderProps) {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
// It takes a cycle for the XY chart to render. This prevents
|
||||
// reporting from printing a blank chart placeholder.
|
||||
useEffect(() => {
|
||||
setIsReady(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<VisualizationContainer isReady={isReady}>
|
||||
<MemoizedChart {...props} />
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function XYChart({ data, args, formatFactory, timeZone }: XYChartRenderProps) {
|
||||
const { legend, layers, isHorizontal } = args;
|
||||
|
||||
if (Object.values(data.tables).every(table => table.rows.length === 0)) {
|
||||
|
|
|
@ -22,13 +22,14 @@ export default function({ getService, loadTestFile }: FtrProviderContext) {
|
|||
|
||||
after(async () => {
|
||||
await esArchiver.unload('logstash_functional');
|
||||
await esArchiver.unload('visualize/default');
|
||||
await esArchiver.unload('lens/basic');
|
||||
});
|
||||
|
||||
describe('', function() {
|
||||
this.tags(['ciGroup4', 'skipFirefox']);
|
||||
|
||||
loadTestFile(require.resolve('./smokescreen'));
|
||||
loadTestFile(require.resolve('./lens_reporting'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
34
x-pack/test/functional/apps/lens/lens_reporting.ts
Normal file
34
x-pack/test/functional/apps/lens/lens_reporting.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['common', 'dashboard', 'reporting']);
|
||||
const find = getService('find');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('lens reporting', () => {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('lens/reporting');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('lens/reporting');
|
||||
});
|
||||
|
||||
it('should not cause PDF reports to fail', async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await PageObjects.dashboard.selectDashboard('Lens reportz');
|
||||
await PageObjects.reporting.openPdfReportingPanel();
|
||||
await PageObjects.reporting.clickGenerateReportButton();
|
||||
|
||||
expect(await find.byButtonText('Download report', undefined, 60000)).to.be.ok();
|
||||
});
|
||||
});
|
||||
}
|
BIN
x-pack/test/functional/es_archives/lens/reporting/data.json.gz
Normal file
BIN
x-pack/test/functional/es_archives/lens/reporting/data.json.gz
Normal file
Binary file not shown.
1165
x-pack/test/functional/es_archives/lens/reporting/mappings.json
Normal file
1165
x-pack/test/functional/es_archives/lens/reporting/mappings.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue