Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Canvas] Generic embeddable function #104499

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,118 @@
/*
* 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 } 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 refName = 'embeddable.id';
const refType = 'embeddable.embeddableType';
const references: SavedObjectReference[] = [
{
name: refName,
type: refType,
id: state.id[0] as string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting an internal server error when attempting to add an embeddable by-reference to a workpad:

server    log   [13:46:48.666] [error][http] TypeError: Cannot read property '0' of undefined
    at ExpressionFunction.extract (/Users/poffdeluxe/Dev/kibana/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts:98:15)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah I've encountered that before. It's throwing the error from the extract function for extracting saved object references. I have a fix for it that went into a different PR. I'll add the fix to this PR too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 81a15dc.

},
];
return {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a "by value" embeddable, then the state should be passed on to the EmbeddablePersistableStateService for extract.

Copy link
Contributor Author

@cqliu1 cqliu1 Sep 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll tackle this in a separate PR to address extracting/injecting references for by-value embeddables.

state: {
...state,
id: [refName],
},
references,
};
},

inject(state, references) {
const reference = references.find((ref) => ref.name === 'embeddable.id');
if (reference) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here should also be passed on to EmbeddablePersistableStateService for inject

state.id[0] = reference.id;
}
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,21 @@
/*
* 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 {
const expressionParts = [] as string[];

expressionParts.push('embeddable');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since there's nothing conditional here, can you just build this as one big string instead of doing the parts+join.

return `embeddable config="${encode(input)}" type="${embeddableType}"`


expressionParts.push(`config="${encode(input)}"`);

expressionParts.push(`type="${embeddableType}"`);

return expressionParts.join(' ');
}
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