From 3bd3364a5567969558245cdfa5a7381ee4ae7c83 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Thu, 19 Mar 2020 09:58:22 -0700 Subject: [PATCH] [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 --- .../expression_types/embeddable_types.ts | 9 +- .../functions/common/index.ts | 2 + .../functions/common/saved_lens.test.ts | 43 ++++++++++ .../functions/common/saved_lens.ts | 83 +++++++++++++++++++ .../renderers/embeddable/embeddable.scss | 33 ++++++++ .../renderers/embeddable/embeddable.tsx | 7 +- .../embeddable_input_to_expression.test.ts | 48 ++++++++++- .../embeddable_input_to_expression.ts | 19 +++++ .../plugins/canvas/common/lib/constants.ts | 1 + .../canvas/i18n/functions/dict/saved_lens.ts | 27 ++++++ .../canvas/i18n/functions/function_help.ts | 2 + .../components/embeddable_flyout/index.tsx | 3 + .../workpad_interactive_page/index.js | 10 ++- .../plugins/canvas/public/style/index.scss | 1 + x-pack/plugins/lens/common/constants.ts | 1 + 15 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts create mode 100644 x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss create mode 100644 x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_lens.ts diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts index d9e841092be56..538aa9f74e2a6 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts @@ -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, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts index 48b50930d563e..36fa6497ab6f3 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts @@ -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, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts new file mode 100644 index 0000000000000..6b197148e6373 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts @@ -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); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts new file mode 100644 index 0000000000000..60026adc0998a --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts @@ -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; + +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, + }; + }, + }; +} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss new file mode 100644 index 0000000000000..04f2f393d1e80 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss @@ -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; + } +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 549e69e57e921..d91e70e43bfd5 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -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 (
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts index 8694c0e2c7f9f..4c622b0c247fa 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.test.ts @@ -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); + }); + }); }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts index a3cb53acebed2..6428507b16a0c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts @@ -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(' '); } diff --git a/x-pack/legacy/plugins/canvas/common/lib/constants.ts b/x-pack/legacy/plugins/canvas/common/lib/constants.ts index 40e143b9ec589..ac8e80b8d7b89 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/constants.ts +++ b/x-pack/legacy/plugins/canvas/common/lib/constants.ts @@ -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`; diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_lens.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_lens.ts new file mode 100644 index 0000000000000..1efcbc9d3a18e --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/saved_lens.ts @@ -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> = { + 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`, + }), + }, +}; diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts b/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts index dbdadd09df67f..e7d7b4ca4321b 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/function_help.ts @@ -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, diff --git a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx index 565ca5fa5bbd6..353a59397d6b6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx @@ -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`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js index b775524acf639..2500a412c0fac 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js @@ -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') + ); } }; diff --git a/x-pack/legacy/plugins/canvas/public/style/index.scss b/x-pack/legacy/plugins/canvas/public/style/index.scss index 4b85620863692..39e5903ff1d96 100644 --- a/x-pack/legacy/plugins/canvas/public/style/index.scss +++ b/x-pack/legacy/plugins/canvas/public/style/index.scss @@ -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'; diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index 57f2a633e4524..16ae1b8da752b 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -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';