[Canvas] Add Lens embeddables (#57499)

* Added lens embeddables to embed flyout

Fixed import

embedded panel styles (#58654)

Merging to WIP draft branch

* Added i18n strings for savedLens

* Added tests for lens embeddables

* Updated tests

* Updated tests

* Added style overrides for lens table

* DDisables triggers on lens emebeddable

* Updated test

* Sets embeddable view mode according to app state

* Fix embeddable component

* Removed embeddable view mode logic

* Removed unused import
This commit is contained in:
Catherine Liu 2020-03-19 09:58:22 -07:00 committed by GitHub
parent fcf439625b
commit 3bd3364a55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 282 additions and 7 deletions

View file

@ -7,9 +7,16 @@
// @ts-ignore
import { MAP_SAVED_OBJECT_TYPE } from '../../../maps/common/constants';
import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/visualizations/public';
import { LENS_EMBEDDABLE_TYPE } from '../../../../../plugins/lens/common/constants';
import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/constants';
export const EmbeddableTypes: { map: string; search: string; visualization: string } = {
export const EmbeddableTypes: {
lens: string;
map: string;
search: string;
visualization: string;
} = {
lens: LENS_EMBEDDABLE_TYPE,
map: MAP_SAVED_OBJECT_TYPE,
search: SEARCH_EMBEDDABLE_TYPE,
visualization: VISUALIZE_EMBEDDABLE_TYPE,

View file

@ -48,6 +48,7 @@ import { rounddate } from './rounddate';
import { rowCount } from './rowCount';
import { repeatImage } from './repeatImage';
import { revealImage } from './revealImage';
import { savedLens } from './saved_lens';
import { savedMap } from './saved_map';
import { savedSearch } from './saved_search';
import { savedVisualization } from './saved_visualization';
@ -109,6 +110,7 @@ export const functions = [
revealImage,
rounddate,
rowCount,
savedLens,
savedMap,
savedSearch,
savedVisualization,

View file

@ -0,0 +1,43 @@
/*
* 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.
*/
jest.mock('ui/new_platform');
import { savedLens } from './saved_lens';
import { getQueryFilters } from '../../../public/lib/build_embeddable_filters';
const filterContext = {
and: [
{ and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' },
{
and: [],
column: 'time-column',
type: 'time',
from: '2019-06-04T04:00:00.000Z',
to: '2019-06-05T04:00:00.000Z',
},
],
};
describe('savedLens', () => {
const fn = savedLens().fn;
const args = {
id: 'some-id',
title: null,
timerange: null,
};
it('accepts null context', () => {
const expression = fn(null, args, {} as any);
expect(expression.input.filters).toEqual([]);
});
it('accepts filter context', () => {
const expression = fn(filterContext, args, {} as any);
const embeddableFilters = getQueryFilters(filterContext.and);
expect(expression.input.filters).toEqual(embeddableFilters);
});
});

View file

@ -0,0 +1,83 @@
/*
* 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 { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { TimeRange } from 'src/plugins/data/public';
import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { getQueryFilters } from '../../../public/lib/build_embeddable_filters';
import { Filter, TimeRange as TimeRangeArg } from '../../../types';
import {
EmbeddableTypes,
EmbeddableExpressionType,
EmbeddableExpression,
} from '../../expression_types';
import { getFunctionHelp } from '../../../i18n';
import { Filter as DataFilter } from '../../../../../../../src/plugins/data/public';
interface Arguments {
id: string;
title: string | null;
timerange: TimeRangeArg | null;
}
export type SavedLensInput = EmbeddableInput & {
id: string;
timeRange?: TimeRange;
filters: DataFilter[];
};
const defaultTimeRange = {
from: 'now-15m',
to: 'now',
};
type Return = EmbeddableExpression<SavedLensInput>;
export function savedLens(): ExpressionFunctionDefinition<
'savedLens',
Filter | null,
Arguments,
Return
> {
const { help, args: argHelp } = getFunctionHelp().savedLens;
return {
name: 'savedLens',
help,
args: {
id: {
types: ['string'],
required: false,
help: argHelp.id,
},
timerange: {
types: ['timerange'],
help: argHelp.timerange,
required: false,
},
title: {
types: ['string'],
help: argHelp.title,
required: false,
},
},
type: EmbeddableExpressionType,
fn: (context, args) => {
const filters = context ? context.and : [];
return {
type: EmbeddableExpressionType,
input: {
id: args.id,
filters: getQueryFilters(filters),
timeRange: args.timerange || defaultTimeRange,
title: args.title ? args.title : undefined,
disableTriggers: true,
},
embeddableType: EmbeddableTypes.lens,
};
},
};
}

View file

@ -0,0 +1,33 @@
.canvasEmbeddable {
.embPanel {
border: none;
background: none;
.embPanel__title {
margin-bottom: $euiSizeXS;
}
.embPanel__optionsMenuButton {
border-radius: $euiBorderRadius;
}
.canvas-isFullscreen & {
.embPanel__optionsMenuButton {
opacity: 0;
}
&:focus .embPanel__optionsMenuButton,
&:hover .embPanel__optionsMenuButton {
opacity: 1;
}
}
}
.euiTable {
background: none;
}
.lnsExpressionRenderer {
@include euiScrollBar;
}
}

View file

@ -18,11 +18,12 @@ import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_a
import { EmbeddableExpression } from '../../expression_types/embeddable';
import { RendererStrings } from '../../../i18n';
import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public';
const { embeddable: strings } = RendererStrings;
import { embeddableInputToExpression } from './embeddable_input_to_expression';
import { EmbeddableInput } from '../../expression_types';
import { RendererHandlers } from '../../../types';
import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib';
const { embeddable: strings } = RendererStrings;
const embeddablesRegistry: {
[key: string]: IEmbeddable;
@ -31,7 +32,7 @@ const embeddablesRegistry: {
const renderEmbeddable = (embeddableObject: IEmbeddable, domNode: HTMLElement) => {
return (
<div
className="embeddable"
className={CANVAS_EMBEDDABLE_CLASSNAME}
style={{ width: domNode.offsetWidth, height: domNode.offsetHeight, cursor: 'auto' }}
>
<I18nContext>

View file

@ -7,12 +7,17 @@
jest.mock('ui/new_platform');
import { embeddableInputToExpression } from './embeddable_input_to_expression';
import { SavedMapInput } from '../../functions/common/saved_map';
import { SavedLensInput } from '../../functions/common/saved_lens';
import { EmbeddableTypes } from '../../expression_types';
import { fromExpression, Ast } from '@kbn/interpreter/common';
const baseSavedMapInput = {
const baseEmbeddableInput = {
id: 'embeddableId',
filters: [],
};
const baseSavedMapInput = {
...baseEmbeddableInput,
isLayerTOCOpen: false,
refreshConfig: {
isPaused: true,
@ -73,4 +78,45 @@ describe('input to expression', () => {
expect(timerangeExpression.chain[0].arguments.to[0]).toEqual(input.timeRange?.to);
});
});
describe('Lens Embeddable', () => {
it('converts to a savedLens expression', () => {
const input: SavedLensInput = {
...baseEmbeddableInput,
};
const expression = embeddableInputToExpression(input, EmbeddableTypes.lens);
const ast = fromExpression(expression);
expect(ast.type).toBe('expression');
expect(ast.chain[0].function).toBe('savedLens');
expect(ast.chain[0].arguments.id).toStrictEqual([input.id]);
expect(ast.chain[0].arguments).not.toHaveProperty('title');
expect(ast.chain[0].arguments).not.toHaveProperty('timerange');
});
it('includes optional input values', () => {
const input: SavedLensInput = {
...baseEmbeddableInput,
title: 'title',
timeRange: {
from: 'now-1h',
to: 'now',
},
};
const expression = embeddableInputToExpression(input, EmbeddableTypes.map);
const ast = fromExpression(expression);
expect(ast.chain[0].arguments).toHaveProperty('title', [input.title]);
expect(ast.chain[0].arguments).toHaveProperty('timerange');
const timerangeExpression = ast.chain[0].arguments.timerange[0] as Ast;
expect(timerangeExpression.chain[0].function).toBe('timerange');
expect(timerangeExpression.chain[0].arguments.from[0]).toEqual(input.timeRange?.from);
expect(timerangeExpression.chain[0].arguments.to[0]).toEqual(input.timeRange?.to);
});
});
});

View file

@ -6,6 +6,7 @@
import { EmbeddableTypes, EmbeddableInput } from '../../expression_types';
import { SavedMapInput } from '../../functions/common/saved_map';
import { SavedLensInput } from '../../functions/common/saved_lens';
/*
Take the input from an embeddable and the type of embeddable and convert it into an expression
@ -46,5 +47,23 @@ export function embeddableInputToExpression(
}
}
if (embeddableType === EmbeddableTypes.lens) {
const lensInput = input as SavedLensInput;
expressionParts.push('savedLens');
expressionParts.push(`id="${input.id}"`);
if (input.title) {
expressionParts.push(`title="${input.title}"`);
}
if (lensInput.timeRange) {
expressionParts.push(
`timerange={timerange from="${lensInput.timeRange.from}" to="${lensInput.timeRange.to}"}`
);
}
}
return expressionParts.join(' ');
}

View file

@ -39,3 +39,4 @@ export const API_ROUTE_SHAREABLE_BASE = '/public/canvas';
export const API_ROUTE_SHAREABLE_ZIP = '/public/canvas/zip';
export const API_ROUTE_SHAREABLE_RUNTIME = '/public/canvas/runtime';
export const API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD = `/public/canvas/${SHAREABLE_RUNTIME_NAME}.js`;
export const CANVAS_EMBEDDABLE_CLASSNAME = `canvasEmbeddable`;

View file

@ -0,0 +1,27 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { savedLens } from '../../../canvas_plugin_src/functions/common/saved_lens';
import { FunctionHelp } from '../function_help';
import { FunctionFactory } from '../../../types';
export const help: FunctionHelp<FunctionFactory<typeof savedLens>> = {
help: i18n.translate('xpack.canvas.functions.savedLensHelpText', {
defaultMessage: `Returns an embeddable for a saved lens object`,
}),
args: {
id: i18n.translate('xpack.canvas.functions.savedLens.args.idHelpText', {
defaultMessage: `The ID of the Saved Lens Object`,
}),
timerange: i18n.translate('xpack.canvas.functions.savedLens.args.timerangeHelpText', {
defaultMessage: `The timerange of data that should be included`,
}),
title: i18n.translate('xpack.canvas.functions.savedLens.args.titleHelpText', {
defaultMessage: `The title for the lens emebeddable`,
}),
},
};

View file

@ -62,6 +62,7 @@ import { help as replace } from './dict/replace';
import { help as revealImage } from './dict/reveal_image';
import { help as rounddate } from './dict/rounddate';
import { help as rowCount } from './dict/row_count';
import { help as savedLens } from './dict/saved_lens';
import { help as savedMap } from './dict/saved_map';
import { help as savedSearch } from './dict/saved_search';
import { help as savedVisualization } from './dict/saved_visualization';
@ -216,6 +217,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({
revealImage,
rounddate,
rowCount,
savedLens,
savedMap,
savedSearch,
savedVisualization,

View file

@ -21,6 +21,9 @@ const allowedEmbeddables = {
[EmbeddableTypes.map]: (id: string) => {
return `savedMap id="${id}" | render`;
},
[EmbeddableTypes.lens]: (id: string) => {
return `savedLens id="${id}" | render`;
},
// FIX: Only currently allow Map embeddables
/* [EmbeddableTypes.visualization]: (id: string) => {
return `filters | savedVisualization id="${id}" | render`;

View file

@ -19,6 +19,7 @@ import {
} from '../../../state/actions/elements';
import { selectToplevelNodes } from '../../../state/actions/transient';
import { crawlTree, globalStateUpdater, shapesForNodes } from '../integration_utils';
import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../../common/lib';
import { InteractiveWorkpadPage as InteractiveComponent } from './interactive_workpad_page';
import { eventHandlers } from './event_handlers';
@ -79,9 +80,14 @@ const isEmbeddableBody = element => {
const hasClosest = typeof element.closest === 'function';
if (hasClosest) {
return element.closest('.embeddable') && !element.closest('.embPanel__header');
return (
element.closest(`.${CANVAS_EMBEDDABLE_CLASSNAME}`) && !element.closest('.embPanel__header')
);
} else {
return closest.call(element, '.embeddable') && !closest.call(element, '.embPanel__header');
return (
closest.call(element, `.${CANVAS_EMBEDDABLE_CLASSNAME}`) &&
!closest.call(element, '.embPanel__header')
);
}
};

View file

@ -61,6 +61,7 @@
@import '../../canvas_plugin_src/renderers/advanced_filter/component/advanced_filter.scss';
@import '../../canvas_plugin_src/renderers/dropdown_filter/component/dropdown_filter.scss';
@import '../../canvas_plugin_src/renderers/embeddable/embeddable.scss';
@import '../../canvas_plugin_src/renderers/plot/plot.scss';
@import '../../canvas_plugin_src/renderers/reveal_image/reveal_image.scss';
@import '../../canvas_plugin_src/renderers/time_filter/components/datetime_calendar/datetime_calendar.scss';

View file

@ -5,6 +5,7 @@
*/
export const PLUGIN_ID = 'lens';
export const LENS_EMBEDDABLE_TYPE = 'lens';
export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations';
export const BASE_APP_URL = '/app/kibana';
export const BASE_API_URL = '/api/lens';