Skip to content

Commit

Permalink
[Canvas] Generic embeddable function (#104499)
Browse files Browse the repository at this point in the history
* Created generic embeddable function

    Fixed telemetry

    Updates expression on input change

    Fixed ts errors

Store embeddable input to expression

Added lib functions

Added comments

Fixed type errors

Fixed ts errors

Clean up

Removed extraneous import

Added context type to embeddable function def

Fix import

Update encode/decode fns

Moved embeddable data url lib file

Added embeddable test

Updated comment

* Fix reference extract/inject in embeddable fn

* Simplify embeddable toExpression

* Moved labsService to flyout.tsx

* Added comment
  • Loading branch information
cqliu1 committed Oct 15, 2021
1 parent 3e6516c commit 6b2bc48
Show file tree
Hide file tree
Showing 17 changed files with 322 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'labs:canvas:byValueEmbeddable': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'labs:canvas:useDataService': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export interface UsageStats {
'banners:textColor': string;
'banners:backgroundColor': string;
'labs:canvas:enable_ui': boolean;
'labs:canvas:byValueEmbeddable': boolean;
'labs:canvas:useDataService': boolean;
'labs:presentation:timeToPresent': boolean;
'labs:dashboard:enable_ui': boolean;
Expand Down
16 changes: 15 additions & 1 deletion src/plugins/presentation_util/common/labs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n';

export const LABS_PROJECT_PREFIX = 'labs:';
export const DEFER_BELOW_FOLD = `${LABS_PROJECT_PREFIX}dashboard:deferBelowFold` as const;
export const BY_VALUE_EMBEDDABLE = `${LABS_PROJECT_PREFIX}canvas:byValueEmbeddable` as const;

export const projectIDs = [DEFER_BELOW_FOLD] as const;
export const projectIDs = [DEFER_BELOW_FOLD, BY_VALUE_EMBEDDABLE] as const;
export const environmentNames = ['kibana', 'browser', 'session'] as const;
export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const;

Expand All @@ -34,6 +35,19 @@ export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = {
}),
solutions: ['dashboard'],
},
[BY_VALUE_EMBEDDABLE]: {
id: BY_VALUE_EMBEDDABLE,
isActive: true,
isDisplayed: true,
environments: ['kibana', 'browser', 'session'],
name: i18n.translate('presentationUtil.labs.enableByValueEmbeddableName', {
defaultMessage: 'By-Value Embeddables',
}),
description: i18n.translate('presentationUtil.labs.enableByValueEmbeddableDescription', {
defaultMessage: 'Enables support for by-value embeddables in Canvas',
}),
solutions: ['canvas'],
},
};

export type ProjectID = typeof projectIDs[number];
Expand Down
6 changes: 6 additions & 0 deletions src/plugins/telemetry/schema/oss_plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -7671,6 +7671,12 @@
"description": "Non-default value of setting."
}
},
"labs:canvas:byValueEmbeddable": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
},
"labs:canvas:useDataService": {
"type": "boolean",
"_meta": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { embeddable } from './embeddable';
import { getQueryFilters } from '../../../common/lib/build_embeddable_filters';
import { ExpressionValueFilter } from '../../../types';
import { encode } from '../../../common/lib/embeddable_dataurl';

const filterContext: ExpressionValueFilter = {
type: 'filter',
and: [
{
type: 'filter',
and: [],
value: 'filter-value',
column: 'filter-column',
filterType: 'exactly',
},
{
type: 'filter',
and: [],
column: 'time-column',
filterType: 'time',
from: '2019-06-04T04:00:00.000Z',
to: '2019-06-05T04:00:00.000Z',
},
],
};

describe('embeddable', () => {
const fn = embeddable().fn;
const config = {
id: 'some-id',
timerange: { from: '15m', to: 'now' },
title: 'test embeddable',
};

const args = {
config: encode(config),
type: 'visualization',
};

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);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { TimeRange } from 'src/plugins/data/public';
import { Filter } from '@kbn/es-query';
import { ExpressionValueFilter } from '../../../types';
import {
EmbeddableExpressionType,
EmbeddableExpression,
EmbeddableInput as Input,
} from '../../expression_types';
import { getFunctionHelp } from '../../../i18n';
import { SavedObjectReference } from '../../../../../../src/core/types';
import { getQueryFilters } from '../../../common/lib/build_embeddable_filters';
import { decode, encode } from '../../../common/lib/embeddable_dataurl';

interface Arguments {
config: string;
type: string;
}

const defaultTimeRange = {
from: 'now-15m',
to: 'now',
};

export type EmbeddableInput = Input & {
timeRange?: TimeRange;
filters?: Filter[];
savedObjectId: string;
};

const baseEmbeddableInput = {
timeRange: defaultTimeRange,
disableTriggers: true,
renderMode: 'noInteractivity',
};

type Return = EmbeddableExpression<EmbeddableInput>;

export function embeddable(): ExpressionFunctionDefinition<
'embeddable',
ExpressionValueFilter | null,
Arguments,
Return
> {
const { help, args: argHelp } = getFunctionHelp().embeddable;

return {
name: 'embeddable',
help,
args: {
config: {
aliases: ['_'],
types: ['string'],
required: true,
help: argHelp.config,
},
type: {
types: ['string'],
required: true,
help: argHelp.type,
},
},
context: {
types: ['filter'],
},
type: EmbeddableExpressionType,
fn: (input, args) => {
const filters = input ? input.and : [];

const embeddableInput = decode(args.config) as EmbeddableInput;

return {
type: EmbeddableExpressionType,
input: {
...baseEmbeddableInput,
...embeddableInput,
filters: getQueryFilters(filters),
},
generatedAt: Date.now(),
embeddableType: args.type,
};
},

extract(state) {
const input = decode(state.config[0] as string);
const refName = 'embeddable.id';

const references: SavedObjectReference[] = [
{
name: refName,
type: state.type[0] as string,
id: input.savedObjectId as string,
},
];

return {
state,
references,
};
},

inject(state, references) {
const reference = references.find((ref) => ref.name === 'embeddable.id');
if (reference) {
const input = decode(state.config[0] as string);
input.savedObjectId = reference.id;
state.config[0] = encode(input);
}
return state;
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ import { savedLens } from './saved_lens';
import { savedMap } from './saved_map';
import { savedSearch } from './saved_search';
import { savedVisualization } from './saved_visualization';
import { embeddable } from './embeddable';

export const functions = [savedLens, savedMap, savedVisualization, savedSearch];
export const functions = [embeddable, savedLens, savedMap, savedSearch, savedVisualization];
2 changes: 2 additions & 0 deletions x-pack/plugins/canvas/canvas_plugin_src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { ChartsPluginStart } from 'src/plugins/charts/public';
import { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public';
import { CanvasSetup } from '../public';
import { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import { UiActionsStart } from '../../../../src/plugins/ui_actions/public';
Expand All @@ -25,6 +26,7 @@ export interface StartDeps {
uiActions: UiActionsStart;
inspector: InspectorStart;
charts: ChartsPluginStart;
presentationUtil: PresentationUtilPluginStart;
}

export type SetupInitializer<T> = (core: CoreSetup<StartDeps>, plugins: SetupDeps) => T;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,19 @@ import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib';

const { embeddable: strings } = RendererStrings;

// registry of references to embeddables on the workpad
const embeddablesRegistry: {
[key: string]: IEmbeddable | Promise<IEmbeddable>;
} = {};

const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
const I18nContext = core.i18n.Context;

return (embeddableObject: IEmbeddable, domNode: HTMLElement) => {
return (embeddableObject: IEmbeddable) => {
return (
<div
className={CANVAS_EMBEDDABLE_CLASSNAME}
style={{ width: domNode.offsetWidth, height: domNode.offsetHeight, cursor: 'auto' }}
style={{ width: '100%', height: '100%', cursor: 'auto' }}
>
<I18nContext>
<plugins.embeddable.EmbeddablePanel embeddable={embeddableObject} />
Expand All @@ -56,6 +57,9 @@ export const embeddableRendererFactory = (
reuseDomNode: true,
render: async (domNode, { input, embeddableType }, handlers) => {
const uniqueId = handlers.getElementId();
const isByValueEnabled = plugins.presentationUtil.labsService.isProjectEnabled(
'labs:canvas:byValueEmbeddable'
);

if (!embeddablesRegistry[uniqueId]) {
const factory = Array.from(plugins.embeddable.getEmbeddableFactories()).find(
Expand All @@ -70,6 +74,7 @@ export const embeddableRendererFactory = (
const embeddablePromise = factory
.createFromSavedObject(input.id, input)
.then((embeddable) => {
// stores embeddable in registrey
embeddablesRegistry[uniqueId] = embeddable;
return embeddable;
});
Expand All @@ -86,23 +91,16 @@ export const embeddableRendererFactory = (
const updatedExpression = embeddableInputToExpression(
updatedInput,
embeddableType,
palettes
palettes,
isByValueEnabled
);

if (updatedExpression) {
handlers.onEmbeddableInputChange(updatedExpression);
}
});

ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () =>
handlers.done()
);

handlers.onResize(() => {
ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () =>
handlers.done()
);
});
ReactDOM.render(renderEmbeddable(embeddableObject), domNode, () => handlers.done());

handlers.onDestroy(() => {
subscription.unsubscribe();
Expand All @@ -115,6 +113,7 @@ export const embeddableRendererFactory = (
} else {
const embeddable = embeddablesRegistry[uniqueId];

// updating embeddable input with changes made to expression or filters
if ('updateInput' in embeddable) {
embeddable.updateInput(input);
embeddable.reload();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { EmbeddableTypes, EmbeddableInput } from '../../expression_types';
import { toExpression as mapToExpression } from './input_type_to_expression/map';
import { toExpression as visualizationToExpression } from './input_type_to_expression/visualization';
import { toExpression as lensToExpression } from './input_type_to_expression/lens';
import { toExpression as genericToExpression } from './input_type_to_expression/embeddable';

export const inputToExpressionTypeMap = {
[EmbeddableTypes.map]: mapToExpression,
Expand All @@ -23,8 +24,13 @@ export const inputToExpressionTypeMap = {
export function embeddableInputToExpression(
input: EmbeddableInput,
embeddableType: string,
palettes: PaletteRegistry
palettes: PaletteRegistry,
useGenericEmbeddable?: boolean
): string | undefined {
if (useGenericEmbeddable) {
return genericToExpression(input, embeddableType);
}

if (inputToExpressionTypeMap[embeddableType]) {
return inputToExpressionTypeMap[embeddableType](input as any, palettes);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { encode } from '../../../../common/lib/embeddable_dataurl';
import { EmbeddableInput } from '../../../expression_types';

export function toExpression(input: EmbeddableInput, embeddableType: string): string {
return `embeddable config="${encode(input)}" type="${embeddableType}"`;
}
13 changes: 13 additions & 0 deletions x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { EmbeddableInput } from '../../canvas_plugin_src/expression_types';

export const encode = (input: Partial<EmbeddableInput>) =>
Buffer.from(JSON.stringify(input)).toString('base64');
export const decode = (serializedInput: string) =>
JSON.parse(Buffer.from(serializedInput, 'base64').toString());
Loading

0 comments on commit 6b2bc48

Please sign in to comment.