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] Extract and inject references for by-value embeddables #115124

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -6,21 +6,11 @@
*/

import { functions as commonFunctions } from '../common';
import { functions as externalFunctions } from '../external';
import { location } from './location';
import { markdown } from './markdown';
import { urlparam } from './urlparam';
import { escount } from './escount';
import { esdocs } from './esdocs';
import { essql } from './essql';

export const functions = [
location,
markdown,
urlparam,
escount,
esdocs,
essql,
...commonFunctions,
...externalFunctions,
];
export const functions = [location, markdown, urlparam, escount, esdocs, essql, ...commonFunctions];
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
* 2.0.
*/

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

const filterContext: ExpressionValueFilter = {
type: 'filter',
Expand All @@ -32,7 +33,7 @@ const filterContext: ExpressionValueFilter = {
};

describe('embeddable', () => {
const fn = embeddable().fn;
const fn = embeddableFunctionFactory({} as InitializeArguments)().fn;
const config = {
id: 'some-id',
timerange: { from: '15m', to: 'now' },
Expand Down
166 changes: 102 additions & 64 deletions x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ 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';
import { InitializeArguments } from '.';

interface Arguments {
export interface Arguments {
config: string;
type: string;
}
Expand All @@ -31,77 +32,114 @@ const baseEmbeddableInput = {

type Return = EmbeddableExpression<EmbeddableInput>;

export function embeddable(): ExpressionFunctionDefinition<
type EmbeddableFunction = 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),
>;

export function embeddableFunctionFactory({
embeddablePersistableStateService,
}: InitializeArguments): () => EmbeddableFunction {
return function embeddable(): EmbeddableFunction {
const { help, args: argHelp } = getFunctionHelp().embeddable;

return {
name: 'embeddable',
help,
args: {
config: {
aliases: ['_'],
types: ['string'],
required: true,
help: argHelp.config,
},
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,
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 {
state,
references,
};
},
return {
type: EmbeddableExpressionType,
input: {
...baseEmbeddableInput,
...embeddableInput,
filters: getQueryFilters(filters),
},
generatedAt: Date.now(),
embeddableType: args.type,
};
},

inject(state, references) {
const reference = references.find((ref) => ref.name === 'embeddable.id');
if (reference) {
extract(state) {
const input = decode(state.config[0] as string);
input.savedObjectId = reference.id;
state.config[0] = encode(input);
}
return state;
},

// extracts references for by-reference embeddables
if (input.savedObjectId) {
const refName = 'embeddable.savedObjectId';
Copy link
Contributor

Choose a reason for hiding this comment

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

These references must get de-duped somewhere in the workpad extract / inject references, but I'm not sure where this happens. Just to make sure: If you look at the workpad in the saved objects explorer, each by reference embeddable has a unique ID?

Copy link
Contributor

Choose a reason for hiding this comment

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

Update: Looks like the de-duping is conducted properly inside the workpad, each reference is prepended with the element ID.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Each reference name includes the unique element ID as a prefix. Here's an example with two elements backed by the same saved object id.

Screen Shot 2021-10-21 at 2 41 40 PM

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks again for double checking this for me!


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

return {
state,
references,
};
}

// extracts references for by-value embeddables
const { state: extractedState, references: extractedReferences } =
embeddablePersistableStateService.extract({
...input,
type: state.type[0],
});

const { type, ...extractedInput } = extractedState;

return {
state: { ...state, config: [encode(extractedInput)], type: [type] },
references: extractedReferences,
};
},

inject(state, references) {
const input = decode(state.config[0] as string);
const savedObjectReference = references.find(
(ref) => ref.name === 'embeddable.savedObjectId'
);

// injects saved object id for by-references embeddable
if (savedObjectReference) {
input.savedObjectId = savedObjectReference.id;
state.config[0] = encode(input);
state.type[0] = savedObjectReference.type;
} else {
// injects references for by-value embeddables
const { type, ...injectedInput } = embeddablePersistableStateService.inject(
Copy link
Contributor

Choose a reason for hiding this comment

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

@cqliu1 I think this is the source of the issue I mentioned to you. inject expects embeddableStateWithType and this is just giving it state.

{ ...input, type: state.type[0] },
references
);
state.config[0] = encode(injectedInput);
state.type[0] = type;
}
return state;
},
};
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,26 @@
* 2.0.
*/

import { EmbeddableStart } from 'src/plugins/embeddable/public';
import { embeddableFunctionFactory } from './embeddable';
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 = [embeddable, savedLens, savedMap, savedSearch, savedVisualization];
export interface InitializeArguments {
embeddablePersistableStateService: {
extract: EmbeddableStart['extract'];
inject: EmbeddableStart['inject'];
};
}

export function initFunctions(initialize: InitializeArguments) {
return [
embeddableFunctionFactory(initialize),
savedLens,
savedMap,
savedSearch,
savedVisualization,
];
}
8 changes: 8 additions & 0 deletions x-pack/plugins/canvas/canvas_plugin_src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { UiActionsStart } from '../../../../src/plugins/ui_actions/public';
import { Start as InspectorStart } from '../../../../src/plugins/inspector/public';

import { functions } from './functions/browser';
import { initFunctions } from './functions/external';
import { typeFunctions } from './expression_types';
import { renderFunctions, renderFunctionFactories } from './renderers';

Expand Down Expand Up @@ -41,6 +42,13 @@ export class CanvasSrcPlugin implements Plugin<void, void, SetupDeps, StartDeps>
plugins.canvas.addRenderers(renderFunctions);

core.getStartServices().then(([coreStart, depsStart]) => {
const externalFunctions = initFunctions({
embeddablePersistableStateService: {
extract: depsStart.embeddable.extract,
inject: depsStart.embeddable.inject,
},
});
plugins.canvas.addFunctions(externalFunctions);
plugins.canvas.addRenderers(
renderFunctionFactories.map((factory: any) => factory(coreStart, depsStart))
);
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
*/

import { i18n } from '@kbn/i18n';
import { embeddable } from '../../../canvas_plugin_src/functions/external/embeddable';
import { embeddableFunctionFactory } from '../../../canvas_plugin_src/functions/external/embeddable';
import { FunctionHelp } from '../function_help';
import { FunctionFactory } from '../../../types';

export const help: FunctionHelp<FunctionFactory<typeof embeddable>> = {
export const help: FunctionHelp<FunctionFactory<ReturnType<typeof embeddableFunctionFactory>>> = {
help: i18n.translate('xpack.canvas.functions.embeddableHelpText', {
defaultMessage: `Returns an embeddable with the provided configuration`,
}),
Expand Down
9 changes: 8 additions & 1 deletion x-pack/plugins/canvas/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { HomeServerPluginSetup } from 'src/plugins/home/server';
import { EmbeddableSetup } from 'src/plugins/embeddable/server';
import { ESSQL_SEARCH_STRATEGY } from '../common/lib/constants';
import { ReportingSetup } from '../../reporting/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
Expand All @@ -30,6 +31,7 @@ import { CanvasRouteHandlerContext, createWorkpadRouteContext } from './workpad_

interface PluginsSetup {
expressions: ExpressionsServerSetup;
embeddable: EmbeddableSetup;
features: FeaturesPluginSetup;
home: HomeServerPluginSetup;
bfetch: BfetchServerSetup;
Expand Down Expand Up @@ -82,7 +84,12 @@ export class CanvasPlugin implements Plugin {
const globalConfig = this.initializerContext.config.legacy.get();
registerCanvasUsageCollector(plugins.usageCollection, globalConfig.kibana.index);

setupInterpreter(expressionsFork);
setupInterpreter(expressionsFork, {
embeddablePersistableStateService: {
extract: plugins.embeddable.extract,
inject: plugins.embeddable.inject,
},
});

coreSetup.getStartServices().then(([_, depsStart]) => {
const strategy = essqlSearchStrategyProvider();
Expand Down
12 changes: 9 additions & 3 deletions x-pack/plugins/canvas/server/setup_interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@

import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { functions } from '../canvas_plugin_src/functions/server';
import { functions as externalFunctions } from '../canvas_plugin_src/functions/external';
import {
initFunctions as initExternalFunctions,
InitializeArguments,
} from '../canvas_plugin_src/functions/external';

export function setupInterpreter(expressions: ExpressionsServerSetup) {
export function setupInterpreter(
expressions: ExpressionsServerSetup,
dependencies: InitializeArguments
) {
functions.forEach((f) => expressions.registerFunction(f));
externalFunctions.forEach((f) => expressions.registerFunction(f));
initExternalFunctions(dependencies).forEach((f) => expressions.registerFunction(f));
}
10 changes: 6 additions & 4 deletions x-pack/plugins/canvas/types/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { UnwrapPromiseOrReturn } from '@kbn/utility-types';
import { functions as commonFunctions } from '../canvas_plugin_src/functions/common';
import { functions as browserFunctions } from '../canvas_plugin_src/functions/browser';
import { functions as serverFunctions } from '../canvas_plugin_src/functions/server';
import { functions as externalFunctions } from '../canvas_plugin_src/functions/external';
import { initFunctions } from '../public/functions';
import { initFunctions as initExternalFunctions } from '../canvas_plugin_src/functions/external';
import { initFunctions as initClientFunctions } from '../public/functions';

/**
* A `ExpressionFunctionFactory` is a powerful type used for any function that produces
Expand Down Expand Up @@ -90,9 +90,11 @@ export type FunctionFactory<FnFactory> =
type CommonFunction = FunctionFactory<typeof commonFunctions[number]>;
type BrowserFunction = FunctionFactory<typeof browserFunctions[number]>;
type ServerFunction = FunctionFactory<typeof serverFunctions[number]>;
type ExternalFunction = FunctionFactory<typeof externalFunctions[number]>;
type ExternalFunction = FunctionFactory<
ReturnType<typeof initExternalFunctions> extends Array<infer U> ? U : never
>;
type ClientFunctions = FunctionFactory<
ReturnType<typeof initFunctions> extends Array<infer U> ? U : never
ReturnType<typeof initClientFunctions> extends Array<infer U> ? U : never
>;

/**
Expand Down