Revert "Revert "[Canvas] By-Value Embeddables (#113827)" (#116527)" (#117613) (#117751)

* Revert "Revert "[Canvas] By-Value Embeddables (#113827)" (#116527)"

This reverts commit 9e6e84571f.

* Fix ts error

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Catherine Liu 2021-11-10 13:06:43 -07:00 committed by GitHub
parent e8b538b27e
commit a4dd96a7b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1328 additions and 126 deletions

View file

@ -231,7 +231,7 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => {
<SolutionToolbarPopover
ownFocus
label={i18n.translate('dashboard.solutionToolbar.editorMenuButtonLabel', {
defaultMessage: 'All types',
defaultMessage: 'Select type',
})}
iconType="arrowDown"
iconSide="right"

View file

@ -444,6 +444,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.' },

View file

@ -122,6 +122,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;

View file

@ -11,7 +11,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 DASHBOARD_CONTROLS = `${LABS_PROJECT_PREFIX}dashboard:dashboardControls` as const;
export const projectIDs = [DEFER_BELOW_FOLD, DASHBOARD_CONTROLS] as const;
export const BY_VALUE_EMBEDDABLE = `${LABS_PROJECT_PREFIX}canvas:byValueEmbeddable` as const;
export const projectIDs = [DEFER_BELOW_FOLD, DASHBOARD_CONTROLS, BY_VALUE_EMBEDDABLE] as const;
export const environmentNames = ['kibana', 'browser', 'session'] as const;
export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const;
@ -48,6 +50,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];

View file

@ -1,9 +1,25 @@
.quickButtonGroup {
.quickButtonGroup__button {
background-color: $euiColorEmptyShade;
// sass-lint:disable-block no-important
border-width: $euiBorderWidthThin !important;
border-style: solid !important;
border-color: $euiBorderColor !important;
.euiButtonGroup__buttons {
border-radius: $euiBorderRadius;
.quickButtonGroup__button {
background-color: $euiColorEmptyShade;
// sass-lint:disable-block no-important
border-width: $euiBorderWidthThin !important;
border-style: solid !important;
border-color: $euiBorderColor !important;
}
.quickButtonGroup__button:first-of-type {
// sass-lint:disable-block no-important
border-top-left-radius: $euiBorderRadius !important;
border-bottom-left-radius: $euiBorderRadius !important;
}
.quickButtonGroup__button:last-of-type {
// sass-lint:disable-block no-important
border-top-right-radius: $euiBorderRadius !important;
border-bottom-right-radius: $euiBorderRadius !important;
}
}
}

View file

@ -7683,6 +7683,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": {

View file

@ -6,7 +6,7 @@
*/
import { ExpressionTypeDefinition } from '../../../../../src/plugins/expressions';
import { EmbeddableInput } from '../../../../../src/plugins/embeddable/common/';
import { EmbeddableInput } from '../../types';
import { EmbeddableTypes } from './embeddable_types';
export const EmbeddableExpressionType = 'embeddable';

View file

@ -6,7 +6,6 @@
*/
import { functions as commonFunctions } from '../common';
import { functions as externalFunctions } from '../external';
import { location } from './location';
import { markdown } from './markdown';
import { urlparam } from './urlparam';
@ -14,13 +13,4 @@ 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];

View file

@ -0,0 +1,60 @@
/*
* 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 { 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',
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 = embeddableFunctionFactory({} as InitializeArguments)().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);
});
});

View file

@ -0,0 +1,145 @@
/*
* 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 { ExpressionValueFilter, EmbeddableInput } from '../../../types';
import { EmbeddableExpressionType, EmbeddableExpression } 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';
import { InitializeArguments } from '.';
export interface Arguments {
config: string;
type: string;
}
const defaultTimeRange = {
from: 'now-15m',
to: 'now',
};
const baseEmbeddableInput = {
timeRange: defaultTimeRange,
disableTriggers: true,
renderMode: 'noInteractivity',
};
type Return = EmbeddableExpression<EmbeddableInput>;
type EmbeddableFunction = ExpressionFunctionDefinition<
'embeddable',
ExpressionValueFilter | null,
Arguments,
Return
>;
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,
},
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);
// extracts references for by-reference embeddables
if (input.savedObjectId) {
const refName = 'embeddable.savedObjectId';
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(
{ ...input, type: state.type[0] },
references
);
state.config[0] = encode(injectedInput);
state.type[0] = type;
}
return state;
},
};
};
}

View file

@ -5,9 +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';
export const functions = [savedLens, savedMap, savedVisualization, savedSearch];
export interface InitializeArguments {
embeddablePersistableStateService: {
extract: EmbeddableStart['extract'];
inject: EmbeddableStart['inject'];
};
}
export function initFunctions(initialize: InitializeArguments) {
return [
embeddableFunctionFactory(initialize),
savedLens,
savedMap,
savedSearch,
savedVisualization,
];
}

View file

@ -9,9 +9,8 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { PaletteOutput } from 'src/plugins/charts/common';
import { Filter as DataFilter } from '@kbn/es-query';
import { TimeRange } from 'src/plugins/data/common';
import { EmbeddableInput } from 'src/plugins/embeddable/common';
import { getQueryFilters } from '../../../common/lib/build_embeddable_filters';
import { ExpressionValueFilter, TimeRange as TimeRangeArg } from '../../../types';
import { ExpressionValueFilter, EmbeddableInput, TimeRange as TimeRangeArg } from '../../../types';
import {
EmbeddableTypes,
EmbeddableExpressionType,
@ -27,7 +26,7 @@ interface Arguments {
}
export type SavedLensInput = EmbeddableInput & {
id: string;
savedObjectId: string;
timeRange?: TimeRange;
filters: DataFilter[];
palette?: PaletteOutput;
@ -73,18 +72,19 @@ export function savedLens(): ExpressionFunctionDefinition<
},
},
type: EmbeddableExpressionType,
fn: (input, args) => {
fn: (input, { id, timerange, title, palette }) => {
const filters = input ? input.and : [];
return {
type: EmbeddableExpressionType,
input: {
id: args.id,
id,
savedObjectId: id,
filters: getQueryFilters(filters),
timeRange: args.timerange || defaultTimeRange,
title: args.title === null ? undefined : args.title,
timeRange: timerange || defaultTimeRange,
title: title === null ? undefined : title,
disableTriggers: true,
palette: args.palette,
palette,
},
embeddableType: EmbeddableTypes.lens,
generatedAt: Date.now(),

View file

@ -30,7 +30,7 @@ const defaultTimeRange = {
to: 'now',
};
type Output = EmbeddableExpression<MapEmbeddableInput>;
type Output = EmbeddableExpression<MapEmbeddableInput & { savedObjectId: string }>;
export function savedMap(): ExpressionFunctionDefinition<
'savedMap',
@ -85,8 +85,9 @@ export function savedMap(): ExpressionFunctionDefinition<
return {
type: EmbeddableExpressionType,
input: {
attributes: { title: '' },
id: args.id,
attributes: { title: '' },
savedObjectId: args.id,
filters: getQueryFilters(filters),
timeRange: args.timerange || defaultTimeRange,
refreshConfig: {

View file

@ -25,7 +25,7 @@ interface Arguments {
title: string | null;
}
type Output = EmbeddableExpression<VisualizeInput>;
type Output = EmbeddableExpression<VisualizeInput & { savedObjectId: string }>;
const defaultTimeRange = {
from: 'now-15m',
@ -94,6 +94,7 @@ export function savedVisualization(): ExpressionFunctionDefinition<
type: EmbeddableExpressionType,
input: {
id,
savedObjectId: id,
disableTriggers: true,
timeRange: timerange || defaultTimeRange,
filters: getQueryFilters(filters),

View file

@ -7,12 +7,14 @@
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';
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';
@ -25,6 +27,7 @@ export interface StartDeps {
uiActions: UiActionsStart;
inspector: InspectorStart;
charts: ChartsPluginStart;
presentationUtil: PresentationUtilPluginStart;
}
export type SetupInitializer<T> = (core: CoreSetup<StartDeps>, plugins: SetupDeps) => T;
@ -39,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))
);

View file

@ -13,16 +13,17 @@ import {
IEmbeddable,
EmbeddableFactory,
EmbeddableFactoryNotFoundError,
isErrorEmbeddable,
} from '../../../../../../src/plugins/embeddable/public';
import { EmbeddableExpression } from '../../expression_types/embeddable';
import { RendererStrings } from '../../../i18n';
import { embeddableInputToExpression } from './embeddable_input_to_expression';
import { EmbeddableInput } from '../../expression_types';
import { RendererFactory } from '../../../types';
import { RendererFactory, EmbeddableInput } from '../../../types';
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>;
} = {};
@ -30,11 +31,11 @@ const embeddablesRegistry: {
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} />
@ -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(
@ -67,15 +71,27 @@ export const embeddableRendererFactory = (
throw new EmbeddableFactoryNotFoundError(embeddableType);
}
const embeddablePromise = factory
.createFromSavedObject(input.id, input)
.then((embeddable) => {
embeddablesRegistry[uniqueId] = embeddable;
return embeddable;
});
embeddablesRegistry[uniqueId] = embeddablePromise;
const embeddableInput = { ...input, id: uniqueId };
const embeddableObject = await (async () => embeddablePromise)();
const embeddablePromise = input.savedObjectId
? factory
.createFromSavedObject(input.savedObjectId, embeddableInput)
.then((embeddable) => {
// stores embeddable in registrey
embeddablesRegistry[uniqueId] = embeddable;
return embeddable;
})
: factory.create(embeddableInput).then((embeddable) => {
if (!embeddable || isErrorEmbeddable(embeddable)) {
return;
}
// stores embeddable in registry
embeddablesRegistry[uniqueId] = embeddable as IEmbeddable;
return embeddable;
});
embeddablesRegistry[uniqueId] = embeddablePromise as Promise<IEmbeddable>;
const embeddableObject = (await (async () => embeddablePromise)()) as IEmbeddable;
const palettes = await plugins.charts.palettes.getPalettes();
@ -86,7 +102,8 @@ export const embeddableRendererFactory = (
const updatedExpression = embeddableInputToExpression(
updatedInput,
embeddableType,
palettes
palettes,
isByValueEnabled
);
if (updatedExpression) {
@ -94,15 +111,7 @@ export const embeddableRendererFactory = (
}
});
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();
@ -115,6 +124,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();

View file

@ -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,
@ -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);
}

View file

@ -0,0 +1,128 @@
/*
* 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 { toExpression } from './embeddable';
import { EmbeddableInput } from '../../../../types';
import { decode } from '../../../../common/lib/embeddable_dataurl';
import { fromExpression } from '@kbn/interpreter/common';
describe('toExpression', () => {
describe('by-reference embeddable input', () => {
const baseEmbeddableInput = {
id: 'elementId',
savedObjectId: 'embeddableId',
filters: [],
};
it('converts to an embeddable expression', () => {
const input: EmbeddableInput = baseEmbeddableInput;
const expression = toExpression(input, 'visualization');
const ast = fromExpression(expression);
expect(ast.type).toBe('expression');
expect(ast.chain[0].function).toBe('embeddable');
expect(ast.chain[0].arguments.type[0]).toBe('visualization');
const config = decode(ast.chain[0].arguments.config[0] as string);
expect(config.savedObjectId).toStrictEqual(input.savedObjectId);
});
it('includes optional input values', () => {
const input: EmbeddableInput = {
...baseEmbeddableInput,
title: 'title',
timeRange: {
from: 'now-1h',
to: 'now',
},
};
const expression = toExpression(input, 'visualization');
const ast = fromExpression(expression);
const config = decode(ast.chain[0].arguments.config[0] as string);
expect(config).toHaveProperty('title', input.title);
expect(config).toHaveProperty('timeRange');
expect(config.timeRange).toHaveProperty('from', input.timeRange?.from);
expect(config.timeRange).toHaveProperty('to', input.timeRange?.to);
});
it('includes empty panel title', () => {
const input: EmbeddableInput = {
...baseEmbeddableInput,
title: '',
};
const expression = toExpression(input, 'visualization');
const ast = fromExpression(expression);
const config = decode(ast.chain[0].arguments.config[0] as string);
expect(config).toHaveProperty('title', input.title);
});
});
describe('by-value embeddable input', () => {
const baseEmbeddableInput = {
id: 'elementId',
disableTriggers: true,
filters: [],
};
it('converts to an embeddable expression', () => {
const input: EmbeddableInput = baseEmbeddableInput;
const expression = toExpression(input, 'visualization');
const ast = fromExpression(expression);
expect(ast.type).toBe('expression');
expect(ast.chain[0].function).toBe('embeddable');
expect(ast.chain[0].arguments.type[0]).toBe('visualization');
const config = decode(ast.chain[0].arguments.config[0] as string);
expect(config.filters).toStrictEqual(input.filters);
expect(config.disableTriggers).toStrictEqual(input.disableTriggers);
});
it('includes optional input values', () => {
const input: EmbeddableInput = {
...baseEmbeddableInput,
title: 'title',
timeRange: {
from: 'now-1h',
to: 'now',
},
};
const expression = toExpression(input, 'visualization');
const ast = fromExpression(expression);
const config = decode(ast.chain[0].arguments.config[0] as string);
expect(config).toHaveProperty('title', input.title);
expect(config).toHaveProperty('timeRange');
expect(config.timeRange).toHaveProperty('from', input.timeRange?.from);
expect(config.timeRange).toHaveProperty('to', input.timeRange?.to);
});
it('includes empty panel title', () => {
const input: EmbeddableInput = {
...baseEmbeddableInput,
title: '',
};
const expression = toExpression(input, 'visualization');
const ast = fromExpression(expression);
const config = decode(ast.chain[0].arguments.config[0] as string);
expect(config).toHaveProperty('title', input.title);
});
});
});

View file

@ -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}"`;
}

View file

@ -11,7 +11,8 @@ import { fromExpression, Ast } from '@kbn/interpreter/common';
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
const baseEmbeddableInput = {
id: 'embeddableId',
id: 'elementId',
savedObjectId: 'embeddableId',
filters: [],
};
@ -27,7 +28,7 @@ describe('toExpression', () => {
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.id).toStrictEqual([input.savedObjectId]);
expect(ast.chain[0].arguments).not.toHaveProperty('title');
expect(ast.chain[0].arguments).not.toHaveProperty('timerange');

View file

@ -14,7 +14,7 @@ export function toExpression(input: SavedLensInput, palettes: PaletteRegistry):
expressionParts.push('savedLens');
expressionParts.push(`id="${input.id}"`);
expressionParts.push(`id="${input.savedObjectId}"`);
if (input.title !== undefined) {
expressionParts.push(`title="${input.title}"`);

View file

@ -6,12 +6,12 @@
*/
import { toExpression } from './map';
import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable';
import { fromExpression, Ast } from '@kbn/interpreter/common';
const baseSavedMapInput = {
id: 'elementId',
attributes: { title: '' },
id: 'embeddableId',
savedObjectId: 'embeddableId',
filters: [],
isLayerTOCOpen: false,
refreshConfig: {
@ -23,7 +23,7 @@ const baseSavedMapInput = {
describe('toExpression', () => {
it('converts to a savedMap expression', () => {
const input: MapEmbeddableInput = {
const input = {
...baseSavedMapInput,
};
@ -33,7 +33,7 @@ describe('toExpression', () => {
expect(ast.type).toBe('expression');
expect(ast.chain[0].function).toBe('savedMap');
expect(ast.chain[0].arguments.id).toStrictEqual([input.id]);
expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]);
expect(ast.chain[0].arguments).not.toHaveProperty('title');
expect(ast.chain[0].arguments).not.toHaveProperty('center');
@ -41,7 +41,7 @@ describe('toExpression', () => {
});
it('includes optional input values', () => {
const input: MapEmbeddableInput = {
const input = {
...baseSavedMapInput,
mapCenter: {
lat: 1,
@ -73,7 +73,7 @@ describe('toExpression', () => {
});
it('includes empty panel title', () => {
const input: MapEmbeddableInput = {
const input = {
...baseSavedMapInput,
title: '',
};

View file

@ -5,13 +5,14 @@
* 2.0.
*/
import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable';
import { MapEmbeddableInput } from '../../../../../../plugins/maps/public';
export function toExpression(input: MapEmbeddableInput): string {
export function toExpression(input: MapEmbeddableInput & { savedObjectId: string }): string {
const expressionParts = [] as string[];
expressionParts.push('savedMap');
expressionParts.push(`id="${input.id}"`);
expressionParts.push(`id="${input.savedObjectId}"`);
if (input.title !== undefined) {
expressionParts.push(`title="${input.title}"`);

View file

@ -9,7 +9,8 @@ import { toExpression } from './visualization';
import { fromExpression, Ast } from '@kbn/interpreter/common';
const baseInput = {
id: 'embeddableId',
id: 'elementId',
savedObjectId: 'embeddableId',
};
describe('toExpression', () => {
@ -24,7 +25,7 @@ describe('toExpression', () => {
expect(ast.type).toBe('expression');
expect(ast.chain[0].function).toBe('savedVisualization');
expect(ast.chain[0].arguments.id).toStrictEqual([input.id]);
expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]);
});
it('includes timerange if given', () => {

View file

@ -7,11 +7,11 @@
import { VisualizeInput } from 'src/plugins/visualizations/public';
export function toExpression(input: VisualizeInput): string {
export function toExpression(input: VisualizeInput & { savedObjectId: string }): string {
const expressionParts = [] as string[];
expressionParts.push('savedVisualization');
expressionParts.push(`id="${input.id}"`);
expressionParts.push(`id="${input.savedObjectId}"`);
if (input.title !== undefined) {
expressionParts.push(`title="${input.title}"`);

View file

@ -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 '../../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());

View file

@ -0,0 +1,25 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { embeddableFunctionFactory } from '../../../canvas_plugin_src/functions/external/embeddable';
import { FunctionHelp } from '../function_help';
import { FunctionFactory } from '../../../types';
export const help: FunctionHelp<FunctionFactory<ReturnType<typeof embeddableFunctionFactory>>> = {
help: i18n.translate('xpack.canvas.functions.embeddableHelpText', {
defaultMessage: `Returns an embeddable with the provided configuration`,
}),
args: {
config: i18n.translate('xpack.canvas.functions.embeddable.args.idHelpText', {
defaultMessage: `The base64 encoded embeddable input object`,
}),
type: i18n.translate('xpack.canvas.functions.embeddable.args.typeHelpText', {
defaultMessage: `The embeddable type`,
}),
},
};

View file

@ -27,6 +27,7 @@ import { help as demodata } from './dict/demodata';
import { help as doFn } from './dict/do';
import { help as dropdownControl } from './dict/dropdown_control';
import { help as eq } from './dict/eq';
import { help as embeddable } from './dict/embeddable';
import { help as escount } from './dict/escount';
import { help as esdocs } from './dict/esdocs';
import { help as essql } from './dict/essql';
@ -182,6 +183,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({
do: doFn,
dropdownControl,
eq,
embeddable,
escount,
esdocs,
essql,

View file

@ -25,6 +25,7 @@
"features",
"inspector",
"presentationUtil",
"visualizations",
"uiActions",
"share"
],

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { FC } from 'react';
import React, { FC, useCallback } from 'react';
import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -27,38 +27,44 @@ const strings = {
};
export interface Props {
onClose: () => void;
onSelect: (id: string, embeddableType: string) => void;
onSelect: (id: string, embeddableType: string, isByValueEnabled?: boolean) => void;
availableEmbeddables: string[];
isByValueEnabled?: boolean;
}
export const AddEmbeddableFlyout: FC<Props> = ({ onSelect, availableEmbeddables, onClose }) => {
export const AddEmbeddableFlyout: FC<Props> = ({
onSelect,
availableEmbeddables,
onClose,
isByValueEnabled,
}) => {
const embeddablesService = useEmbeddablesService();
const platformService = usePlatformService();
const { getEmbeddableFactories } = embeddablesService;
const { getSavedObjects, getUISettings } = platformService;
const onAddPanel = (id: string, savedObjectType: string, name: string) => {
const embeddableFactories = getEmbeddableFactories();
const onAddPanel = useCallback(
(id: string, savedObjectType: string) => {
const embeddableFactories = getEmbeddableFactories();
// Find the embeddable type from the saved object type
const found = Array.from(embeddableFactories).find((embeddableFactory) => {
return Boolean(
embeddableFactory.savedObjectMetaData &&
embeddableFactory.savedObjectMetaData.type === savedObjectType
);
});
// Find the embeddable type from the saved object type
const found = Array.from(embeddableFactories).find((embeddableFactory) => {
return Boolean(
embeddableFactory.savedObjectMetaData &&
embeddableFactory.savedObjectMetaData.type === savedObjectType
);
});
const foundEmbeddableType = found ? found.type : 'unknown';
const foundEmbeddableType = found ? found.type : 'unknown';
onSelect(id, foundEmbeddableType);
};
onSelect(id, foundEmbeddableType, isByValueEnabled);
},
[isByValueEnabled, getEmbeddableFactories, onSelect]
);
const embeddableFactories = getEmbeddableFactories();
const availableSavedObjects = Array.from(embeddableFactories)
.filter((factory) => {
return availableEmbeddables.includes(factory.type);
})
.filter((factory) => isByValueEnabled || availableEmbeddables.includes(factory.type))
.map((factory) => factory.savedObjectMetaData)
.filter<SavedObjectMetaData<{}>>(function (
maybeSavedObjectMetaData

View file

@ -8,12 +8,14 @@
import React, { useMemo, useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { useSelector, useDispatch } from 'react-redux';
import { encode } from '../../../common/lib/embeddable_dataurl';
import { AddEmbeddableFlyout as Component, Props as ComponentProps } from './flyout.component';
// @ts-expect-error untyped local
import { addElement } from '../../state/actions/elements';
import { getSelectedPage } from '../../state/selectors/workpad';
import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable';
import { State } from '../../../types';
import { useLabsService } from '../../services';
const allowedEmbeddables = {
[EmbeddableTypes.map]: (id: string) => {
@ -65,6 +67,9 @@ export const AddEmbeddablePanel: React.FunctionComponent<FlyoutProps> = ({
availableEmbeddables,
...restProps
}) => {
const labsService = useLabsService();
const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable');
const dispatch = useDispatch();
const pageId = useSelector<State, string>((state) => getSelectedPage(state));
@ -74,18 +79,27 @@ export const AddEmbeddablePanel: React.FunctionComponent<FlyoutProps> = ({
);
const onSelect = useCallback(
(id: string, type: string) => {
(id: string, type: string): void => {
const partialElement = {
expression: `markdown "Could not find embeddable for type ${type}" | render`,
};
if (allowedEmbeddables[type]) {
// If by-value is enabled, we'll handle both by-reference and by-value embeddables
// with the new generic `embeddable` function.
// Otherwise we fallback to the embeddable type specific expressions.
if (isByValueEnabled) {
const config = encode({ savedObjectId: id });
partialElement.expression = `embeddable config="${config}"
type="${type}"
| render`;
} else if (allowedEmbeddables[type]) {
partialElement.expression = allowedEmbeddables[type](id);
}
addEmbeddable(pageId, partialElement);
restProps.onClose();
},
[addEmbeddable, pageId, restProps]
[addEmbeddable, pageId, restProps, isByValueEnabled]
);
return (
@ -93,6 +107,7 @@ export const AddEmbeddablePanel: React.FunctionComponent<FlyoutProps> = ({
{...restProps}
availableEmbeddables={availableEmbeddables || []}
onSelect={onSelect}
isByValueEnabled={isByValueEnabled}
/>
);
};

View file

@ -6,3 +6,5 @@
*/
export { useDownloadWorkpad, useDownloadRenderedWorkpad } from './use_download_workpad';
export { useIncomingEmbeddable } from './use_incoming_embeddable';

View file

@ -0,0 +1,86 @@
/*
* 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 { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { fromExpression } from '@kbn/interpreter/common';
import { CANVAS_APP } from '../../../../common/lib';
import { decode, encode } from '../../../../common/lib/embeddable_dataurl';
import { CanvasElement, CanvasPage } from '../../../../types';
import { useEmbeddablesService, useLabsService } from '../../../services';
// @ts-expect-error unconverted file
import { addElement } from '../../../state/actions/elements';
// @ts-expect-error unconverted file
import { selectToplevelNodes } from '../../../state/actions/transient';
import {
updateEmbeddableExpression,
fetchEmbeddableRenderable,
} from '../../../state/actions/embeddable';
import { clearValue } from '../../../state/actions/resolved_args';
export const useIncomingEmbeddable = (selectedPage: CanvasPage) => {
const embeddablesService = useEmbeddablesService();
const labsService = useLabsService();
const dispatch = useDispatch();
const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable');
const stateTransferService = embeddablesService.getStateTransfer();
// fetch incoming embeddable from state transfer service.
const incomingEmbeddable = stateTransferService.getIncomingEmbeddablePackage(CANVAS_APP, true);
useEffect(() => {
if (isByValueEnabled && incomingEmbeddable) {
const { embeddableId, input: incomingInput, type } = incomingEmbeddable;
// retrieve existing element
const originalElement = selectedPage.elements.find(
({ id }: CanvasElement) => id === embeddableId
);
if (originalElement) {
const originalAst = fromExpression(originalElement!.expression);
const functionIndex = originalAst.chain.findIndex(
({ function: fn }) => fn === 'embeddable'
);
const originalInput = decode(
originalAst.chain[functionIndex].arguments.config[0] as string
);
// clear out resolved arg for old embeddable
const argumentPath = [embeddableId, 'expressionRenderable'];
dispatch(clearValue({ path: argumentPath }));
const updatedInput = { ...originalInput, ...incomingInput };
const expression = `embeddable config="${encode(updatedInput)}"
type="${type}"
| render`;
dispatch(
updateEmbeddableExpression({
elementId: originalElement.id,
embeddableExpression: expression,
})
);
// update resolved args
dispatch(fetchEmbeddableRenderable(originalElement.id));
// select new embeddable element
dispatch(selectToplevelNodes([embeddableId]));
} else {
const expression = `embeddable config="${encode(incomingInput)}"
type="${type}"
| render`;
dispatch(addElement(selectedPage.id, { expression }));
}
}
}, [dispatch, selectedPage, incomingEmbeddable, isByValueEnabled]);
};

View file

@ -27,6 +27,7 @@ import { WorkpadRoutingContext } from '../../routes/workpad';
import { usePlatformService } from '../../services';
import { Workpad as WorkpadComponent, Props } from './workpad.component';
import { State } from '../../../types';
import { useIncomingEmbeddable } from '../hooks';
type ContainerProps = Pick<Props, 'registerLayout' | 'unregisterLayout'>;
@ -58,6 +59,9 @@ export const Workpad: FC<ContainerProps> = (props) => {
};
});
const selectedPage = propsFromState.pages[propsFromState.selectedPageNumber - 1];
useIncomingEmbeddable(selectedPage);
const fetchAllRenderables = useCallback(() => {
dispatch(fetchAllRenderablesAction());
}, [dispatch]);

View file

@ -31,7 +31,7 @@ $canvasLayoutFontSize: $euiFontSizeS;
.canvasLayout__stageHeader {
flex-grow: 0;
flex-basis: auto;
padding: $euiSizeS;
padding: $euiSizeS $euiSize;
font-size: $canvasLayoutFontSize;
border-bottom: $euiBorderThin;
background: $euiColorLightestShade;

View file

@ -0,0 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots components/WorkpadHeader/EditorMenu dark mode 1`] = `
<div
className="euiPopover euiPopover--anchorDownLeft"
data-test-subj="canvasEditorMenuButton"
>
<div
className="euiPopover__anchor"
>
<button
className="euiButton euiButton--text solutionToolbarButton undefined"
data-test-subj="canvasEditorMenuButton"
disabled={false}
onClick={[Function]}
style={
Object {
"minWidth": undefined,
}
}
type="button"
>
<span
className="euiButtonContent euiButtonContent--iconRight euiButton__content"
>
<span
className="euiButtonContent__icon"
color="inherit"
data-euiicon-type="arrowDown"
size="m"
/>
<span
className="euiButton__text"
>
Select type
</span>
</span>
</button>
</div>
</div>
`;
exports[`Storyshots components/WorkpadHeader/EditorMenu default 1`] = `
<div
className="euiPopover euiPopover--anchorDownLeft"
data-test-subj="canvasEditorMenuButton"
>
<div
className="euiPopover__anchor"
>
<button
className="euiButton euiButton--text solutionToolbarButton undefined"
data-test-subj="canvasEditorMenuButton"
disabled={false}
onClick={[Function]}
style={
Object {
"minWidth": undefined,
}
}
type="button"
>
<span
className="euiButtonContent euiButtonContent--iconRight euiButton__content"
>
<span
className="euiButtonContent__icon"
color="inherit"
data-euiicon-type="arrowDown"
size="m"
/>
<span
className="euiButton__text"
>
Select type
</span>
</span>
</button>
</div>
</div>
`;

View file

@ -0,0 +1,107 @@
/*
* 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 { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import React from 'react';
import { EmbeddableFactoryDefinition, IEmbeddable } from 'src/plugins/embeddable/public';
import { BaseVisType, VisTypeAlias } from 'src/plugins/visualizations/public';
import { EditorMenu } from '../editor_menu.component';
const testFactories: EmbeddableFactoryDefinition[] = [
{
type: 'ml_anomaly_swimlane',
getDisplayName: () => 'Anomaly swimlane',
getIconType: () => '',
getDescription: () => 'Description for anomaly swimlane',
isEditable: () => Promise.resolve(true),
create: () => Promise.resolve({ id: 'swimlane_embeddable' } as IEmbeddable),
grouping: [
{
id: 'ml',
getDisplayName: () => 'machine learning',
getIconType: () => 'machineLearningApp',
},
],
},
{
type: 'ml_anomaly_chart',
getDisplayName: () => 'Anomaly chart',
getIconType: () => '',
getDescription: () => 'Description for anomaly chart',
isEditable: () => Promise.resolve(true),
create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable),
grouping: [
{
id: 'ml',
getDisplayName: () => 'machine learning',
getIconType: () => 'machineLearningApp',
},
],
},
{
type: 'log_stream',
getDisplayName: () => 'Log stream',
getIconType: () => '',
getDescription: () => 'Description for log stream',
isEditable: () => Promise.resolve(true),
create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable),
},
];
const testVisTypes: BaseVisType[] = [
{ title: 'TSVB', icon: '', description: 'Description of TSVB', name: 'tsvb' } as BaseVisType,
{
titleInWizard: 'Custom visualization',
title: 'Vega',
icon: '',
description: 'Description of Vega',
name: 'vega',
} as BaseVisType,
];
const testVisTypeAliases: VisTypeAlias[] = [
{
title: 'Lens',
aliasApp: 'lens',
aliasPath: 'path/to/lens',
icon: 'lensApp',
name: 'lens',
description: 'Description of Lens app',
stage: 'production',
},
{
title: 'Maps',
aliasApp: 'maps',
aliasPath: 'path/to/maps',
icon: 'gisApp',
name: 'maps',
description: 'Description of Maps app',
stage: 'production',
},
];
storiesOf('components/WorkpadHeader/EditorMenu', module)
.add('default', () => (
<EditorMenu
factories={testFactories}
promotedVisTypes={testVisTypes}
visTypeAliases={testVisTypeAliases}
createNewVisType={() => action('createNewVisType')}
createNewEmbeddable={() => action('createNewEmbeddable')}
/>
))
.add('dark mode', () => (
<EditorMenu
factories={testFactories}
isDarkThemeEnabled
promotedVisTypes={testVisTypes}
visTypeAliases={testVisTypeAliases}
createNewVisType={() => action('createNewVisType')}
createNewEmbeddable={() => action('createNewEmbeddable')}
/>
));

View file

@ -0,0 +1,170 @@
/*
* 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 React, { FC } from 'react';
import {
EuiContextMenu,
EuiContextMenuPanelItemDescriptor,
EuiContextMenuItemIcon,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EmbeddableFactoryDefinition } from '../../../../../../../src/plugins/embeddable/public';
import { BaseVisType, VisTypeAlias } from '../../../../../../../src/plugins/visualizations/public';
import { SolutionToolbarPopover } from '../../../../../../../src/plugins/presentation_util/public';
const strings = {
getEditorMenuButtonLabel: () =>
i18n.translate('xpack.canvas.solutionToolbar.editorMenuButtonLabel', {
defaultMessage: 'Select type',
}),
};
interface FactoryGroup {
id: string;
appName: string;
icon: EuiContextMenuItemIcon;
panelId: number;
factories: EmbeddableFactoryDefinition[];
}
interface Props {
factories: EmbeddableFactoryDefinition[];
isDarkThemeEnabled?: boolean;
promotedVisTypes: BaseVisType[];
visTypeAliases: VisTypeAlias[];
createNewVisType: (visType?: BaseVisType | VisTypeAlias) => () => void;
createNewEmbeddable: (factory: EmbeddableFactoryDefinition) => () => void;
}
export const EditorMenu: FC<Props> = ({
factories,
isDarkThemeEnabled,
promotedVisTypes,
visTypeAliases,
createNewVisType,
createNewEmbeddable,
}: Props) => {
const factoryGroupMap: Record<string, FactoryGroup> = {};
const ungroupedFactories: EmbeddableFactoryDefinition[] = [];
let panelCount = 1;
// Maps factories with a group to create nested context menus for each group type
// and pushes ungrouped factories into a separate array
factories.forEach((factory: EmbeddableFactoryDefinition, index) => {
const { grouping } = factory;
if (grouping) {
grouping.forEach((group) => {
if (factoryGroupMap[group.id]) {
factoryGroupMap[group.id].factories.push(factory);
} else {
factoryGroupMap[group.id] = {
id: group.id,
appName: group.getDisplayName ? group.getDisplayName({}) : group.id,
icon: (group.getIconType ? group.getIconType({}) : 'empty') as EuiContextMenuItemIcon,
factories: [factory],
panelId: panelCount,
};
panelCount++;
}
});
} else {
ungroupedFactories.push(factory);
}
});
const getVisTypeMenuItem = (visType: BaseVisType): EuiContextMenuPanelItemDescriptor => {
const { name, title, titleInWizard, description, icon = 'empty' } = visType;
return {
name: titleInWizard || title,
icon: icon as string,
onClick: createNewVisType(visType),
'data-test-subj': `visType-${name}`,
toolTipContent: description,
};
};
const getVisTypeAliasMenuItem = (
visTypeAlias: VisTypeAlias
): EuiContextMenuPanelItemDescriptor => {
const { name, title, description, icon = 'empty' } = visTypeAlias;
return {
name: title,
icon,
onClick: createNewVisType(visTypeAlias),
'data-test-subj': `visType-${name}`,
toolTipContent: description,
};
};
const getEmbeddableFactoryMenuItem = (
factory: EmbeddableFactoryDefinition
): EuiContextMenuPanelItemDescriptor => {
const icon = factory?.getIconType ? factory.getIconType() : 'empty';
const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined;
return {
name: factory.getDisplayName(),
icon,
toolTipContent,
onClick: createNewEmbeddable(factory),
'data-test-subj': `createNew-${factory.type}`,
};
};
const editorMenuPanels = [
{
id: 0,
items: [
...visTypeAliases.map(getVisTypeAliasMenuItem),
...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({
name: appName,
icon,
panel: panelId,
'data-test-subj': `canvasEditorMenu-${id}Group`,
})),
...ungroupedFactories.map(getEmbeddableFactoryMenuItem),
...promotedVisTypes.map(getVisTypeMenuItem),
],
},
...Object.values(factoryGroupMap).map(
({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({
id: panelId,
title: appName,
items: groupFactories.map(getEmbeddableFactoryMenuItem),
})
),
];
return (
<SolutionToolbarPopover
ownFocus
label={strings.getEditorMenuButtonLabel()}
iconType="arrowDown"
iconSide="right"
panelPaddingSize="none"
data-test-subj="canvasEditorMenuButton"
>
{() => (
<EuiContextMenu
initialPanelId={0}
panels={editorMenuPanels}
className={`canvasSolutionToolbar__editorContextMenu ${
isDarkThemeEnabled
? 'canvasSolutionToolbar__editorContextMenu--dark'
: 'canvasSolutionToolbar__editorContextMenu--light'
}`}
data-test-subj="canvasEditorContextMenu"
/>
)}
</SolutionToolbarPopover>
);
};

View file

@ -0,0 +1,147 @@
/*
* 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 React, { FC, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { trackCanvasUiMetric, METRIC_TYPE } from '../../../../public/lib/ui_metric';
import {
useEmbeddablesService,
usePlatformService,
useVisualizationsService,
} from '../../../services';
import {
BaseVisType,
VisGroups,
VisTypeAlias,
} from '../../../../../../../src/plugins/visualizations/public';
import {
EmbeddableFactoryDefinition,
EmbeddableInput,
} from '../../../../../../../src/plugins/embeddable/public';
import { CANVAS_APP } from '../../../../common/lib';
import { encode } from '../../../../common/lib/embeddable_dataurl';
import { ElementSpec } from '../../../../types';
import { EditorMenu as Component } from './editor_menu.component';
interface Props {
/**
* Handler for adding a selected element to the workpad
*/
addElement: (element: Partial<ElementSpec>) => void;
}
export const EditorMenu: FC<Props> = ({ addElement }) => {
const embeddablesService = useEmbeddablesService();
const { pathname, search } = useLocation();
const platformService = usePlatformService();
const stateTransferService = embeddablesService.getStateTransfer();
const visualizationsService = useVisualizationsService();
const IS_DARK_THEME = platformService.getUISetting('theme:darkMode');
const createNewVisType = useCallback(
(visType?: BaseVisType | VisTypeAlias) => () => {
let path = '';
let appId = '';
if (visType) {
if (trackCanvasUiMetric) {
trackCanvasUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`);
}
if ('aliasPath' in visType) {
appId = visType.aliasApp;
path = visType.aliasPath;
} else {
appId = 'visualize';
path = `#/create?type=${encodeURIComponent(visType.name)}`;
}
} else {
appId = 'visualize';
path = '#/create?';
}
stateTransferService.navigateToEditor(appId, {
path,
state: {
originatingApp: CANVAS_APP,
originatingPath: `#/${pathname}${search}`,
},
});
},
[stateTransferService, pathname, search]
);
const createNewEmbeddable = useCallback(
(factory: EmbeddableFactoryDefinition) => async () => {
if (trackCanvasUiMetric) {
trackCanvasUiMetric(METRIC_TYPE.CLICK, factory.type);
}
let embeddableInput;
if (factory.getExplicitInput) {
embeddableInput = await factory.getExplicitInput();
} else {
const newEmbeddable = await factory.create({} as EmbeddableInput);
embeddableInput = newEmbeddable?.getInput();
}
if (embeddableInput) {
const config = encode(embeddableInput);
const expression = `embeddable config="${config}"
type="${factory.type}"
| render`;
addElement({ expression });
}
},
[addElement]
);
const getVisTypesByGroup = (group: VisGroups): BaseVisType[] =>
visualizationsService
.getByGroup(group)
.sort(({ name: a }: BaseVisType | VisTypeAlias, { name: b }: BaseVisType | VisTypeAlias) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
})
.filter(({ hidden }: BaseVisType) => !hidden);
const visTypeAliases = visualizationsService
.getAliases()
.sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) =>
a === b ? 0 : a ? -1 : 1
);
const factories = embeddablesService
? Array.from(embeddablesService.getEmbeddableFactories()).filter(
({ type, isEditable, canCreateNew, isContainerType }) =>
isEditable() &&
!isContainerType &&
canCreateNew() &&
!['visualization', 'ml'].some((factoryType) => {
return type.includes(factoryType);
})
)
: [];
const promotedVisTypes = getVisTypesByGroup(VisGroups.PROMOTED);
return (
<Component
createNewVisType={createNewVisType}
createNewEmbeddable={createNewEmbeddable}
promotedVisTypes={promotedVisTypes}
isDarkThemeEnabled={IS_DARK_THEME}
factories={factories}
visTypeAliases={visTypeAliases}
/>
);
};

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { EditorMenu } from './editor_menu';
export { EditorMenu as EditorMenuComponent } from './editor_menu.component';

View file

@ -12,11 +12,11 @@ import { EuiContextMenu, EuiIcon, EuiContextMenuPanelItemDescriptor } from '@ela
import { i18n } from '@kbn/i18n';
import { PrimaryActionPopover } from '../../../../../../../src/plugins/presentation_util/public';
import { getId } from '../../../lib/get_id';
import { ClosePopoverFn } from '../../popover';
import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib';
import { ElementSpec } from '../../../../types';
import { flattenPanelTree } from '../../../lib/flatten_panel_tree';
import { AssetManager } from '../../asset_manager';
import { ClosePopoverFn } from '../../popover';
import { SavedElementsModal } from '../../saved_elements_modal';
interface CategorizedElementLists {
@ -112,7 +112,7 @@ const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: Ele
return categories;
};
interface Props {
export interface Props {
/**
* Dictionary of elements from elements registry
*/
@ -120,7 +120,7 @@ interface Props {
/**
* Handler for adding a selected element to the workpad
*/
addElement: (element: ElementSpec) => void;
addElement: (element: Partial<ElementSpec>) => void;
}
export const ElementMenu: FunctionComponent<Props> = ({ elements, addElement }) => {

View file

@ -5,5 +5,4 @@
* 2.0.
*/
export { ElementMenu } from './element_menu';
export { ElementMenu as ElementMenuComponent } from './element_menu.component';
export { ElementMenu } from './element_menu.component';

View file

@ -27,6 +27,7 @@ import { ElementMenu } from './element_menu';
import { ShareMenu } from './share_menu';
import { ViewMenu } from './view_menu';
import { LabsControl } from './labs_control';
import { EditorMenu } from './editor_menu';
const strings = {
getFullScreenButtonAriaLabel: () =>
@ -160,24 +161,22 @@ export const WorkpadHeader: FC<Props> = ({
<EuiFlexGroup
gutterSize="none"
alignItems="center"
justifyContent="spaceBetween"
className="canvasLayout__stageHeaderInner"
>
{isWriteable && (
<EuiFlexItem grow={false}>
<SolutionToolbar>
{{
primaryActionButton: <ElementMenu addElement={addElement} elements={elements} />,
quickButtonGroup: <QuickButtonGroup buttons={quickButtons} />,
addFromLibraryButton: <AddFromLibraryButton onClick={showEmbedPanel} />,
extraButtons: [<EditorMenu addElement={addElement} />],
}}
</SolutionToolbar>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="none">
{isWriteable && (
<EuiFlexItem>
<SolutionToolbar>
{{
primaryActionButton: (
<ElementMenu addElement={addElement} elements={elements} />
),
quickButtonGroup: <QuickButtonGroup buttons={quickButtons} />,
addFromLibraryButton: <AddFromLibraryButton onClick={showEmbedPanel} />,
}}
</SolutionToolbar>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<ViewMenu />
</EuiFlexItem>
@ -192,6 +191,7 @@ export const WorkpadHeader: FC<Props> = ({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem />
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>

View file

@ -8,6 +8,7 @@
import { BehaviorSubject } from 'rxjs';
import type { SharePluginSetup } from 'src/plugins/share/public';
import { ChartsPluginSetup, ChartsPluginStart } from 'src/plugins/charts/public';
import { VisualizationsStart } from 'src/plugins/visualizations/public';
import { ReportingStart } from '../../reporting/public';
import {
CoreSetup,
@ -63,6 +64,7 @@ export interface CanvasStartDeps {
charts: ChartsPluginStart;
data: DataPublicPluginStart;
presentationUtil: PresentationUtilPluginStart;
visualizations: VisualizationsStart;
spaces?: SpacesPluginStart;
}
@ -122,7 +124,12 @@ export class CanvasPlugin
const { pluginServices } = await import('./services');
pluginServices.setRegistry(
pluginServiceRegistry.start({ coreStart, startPlugins, initContext: this.initContext })
pluginServiceRegistry.start({
coreStart,
startPlugins,
appUpdater: this.appUpdater,
initContext: this.initContext,
})
);
// Load application bundle

View file

@ -50,7 +50,7 @@ export const useWorkpad = (
setResolveInfo({ aliasId, outcome, id: workpadId });
// If it's an alias match, we know we are going to redirect so don't even dispatch that we got the workpad
if (outcome !== 'aliasMatch') {
if (storedWorkpad.id !== workpadId && outcome !== 'aliasMatch') {
workpad.aliasId = aliasId;
dispatch(setAssets(assets));
@ -61,7 +61,7 @@ export const useWorkpad = (
setError(e as Error | string);
}
})();
}, [workpadId, dispatch, setError, loadPages, workpadResolve]);
}, [workpadId, dispatch, setError, loadPages, workpadResolve, storedWorkpad.id]);
useEffect(() => {
// If the resolved info is not for the current workpad id, bail out

View file

@ -5,8 +5,12 @@
* 2.0.
*/
import { EmbeddableFactory } from '../../../../../src/plugins/embeddable/public';
import {
EmbeddableFactory,
EmbeddableStateTransfer,
} from '../../../../../src/plugins/embeddable/public';
export interface CanvasEmbeddablesService {
getEmbeddableFactories: () => IterableIterator<EmbeddableFactory>;
getStateTransfer: () => EmbeddableStateTransfer;
}

View file

@ -17,6 +17,7 @@ import { CanvasNavLinkService } from './nav_link';
import { CanvasNotifyService } from './notify';
import { CanvasPlatformService } from './platform';
import { CanvasReportingService } from './reporting';
import { CanvasVisualizationsService } from './visualizations';
import { CanvasWorkpadService } from './workpad';
export interface CanvasPluginServices {
@ -28,6 +29,7 @@ export interface CanvasPluginServices {
notify: CanvasNotifyService;
platform: CanvasPlatformService;
reporting: CanvasReportingService;
visualizations: CanvasVisualizationsService;
workpad: CanvasWorkpadService;
}
@ -44,4 +46,6 @@ export const useNavLinkService = () => (() => pluginServices.getHooks().navLink.
export const useNotifyService = () => (() => pluginServices.getHooks().notify.useService())();
export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())();
export const useReportingService = () => (() => pluginServices.getHooks().reporting.useService())();
export const useVisualizationsService = () =>
(() => pluginServices.getHooks().visualizations.useService())();
export const useWorkpadService = () => (() => pluginServices.getHooks().workpad.useService())();

View file

@ -16,4 +16,5 @@ export type EmbeddablesServiceFactory = KibanaPluginServiceFactory<
export const embeddablesServiceFactory: EmbeddablesServiceFactory = ({ startPlugins }) => ({
getEmbeddableFactories: startPlugins.embeddable.getEmbeddableFactories,
getStateTransfer: startPlugins.embeddable.getStateTransfer,
});

View file

@ -22,6 +22,7 @@ import { navLinkServiceFactory } from './nav_link';
import { notifyServiceFactory } from './notify';
import { platformServiceFactory } from './platform';
import { reportingServiceFactory } from './reporting';
import { visualizationsServiceFactory } from './visualizations';
import { workpadServiceFactory } from './workpad';
export { customElementServiceFactory } from './custom_element';
@ -31,6 +32,7 @@ export { labsServiceFactory } from './labs';
export { notifyServiceFactory } from './notify';
export { platformServiceFactory } from './platform';
export { reportingServiceFactory } from './reporting';
export { visualizationsServiceFactory } from './visualizations';
export { workpadServiceFactory } from './workpad';
export const pluginServiceProviders: PluginServiceProviders<
@ -45,6 +47,7 @@ export const pluginServiceProviders: PluginServiceProviders<
notify: new PluginServiceProvider(notifyServiceFactory),
platform: new PluginServiceProvider(platformServiceFactory),
reporting: new PluginServiceProvider(reportingServiceFactory),
visualizations: new PluginServiceProvider(visualizationsServiceFactory),
workpad: new PluginServiceProvider(workpadServiceFactory),
};

View file

@ -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 { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public';
import { CanvasStartDeps } from '../../plugin';
import { CanvasVisualizationsService } from '../visualizations';
export type VisualizationsServiceFactory = KibanaPluginServiceFactory<
CanvasVisualizationsService,
CanvasStartDeps
>;
export const visualizationsServiceFactory: VisualizationsServiceFactory = ({ startPlugins }) => ({
showNewVisModal: startPlugins.visualizations.showNewVisModal,
getByGroup: startPlugins.visualizations.getByGroup,
getAliases: startPlugins.visualizations.getAliases,
});

View file

@ -14,4 +14,5 @@ const noop = (..._args: any[]): any => {};
export const embeddablesServiceFactory: EmbeddablesServiceFactory = () => ({
getEmbeddableFactories: noop,
getStateTransfer: noop,
});

View file

@ -22,6 +22,7 @@ import { navLinkServiceFactory } from './nav_link';
import { notifyServiceFactory } from './notify';
import { platformServiceFactory } from './platform';
import { reportingServiceFactory } from './reporting';
import { visualizationsServiceFactory } from './visualizations';
import { workpadServiceFactory } from './workpad';
export { customElementServiceFactory } from './custom_element';
@ -31,6 +32,7 @@ export { navLinkServiceFactory } from './nav_link';
export { notifyServiceFactory } from './notify';
export { platformServiceFactory } from './platform';
export { reportingServiceFactory } from './reporting';
export { visualizationsServiceFactory } from './visualizations';
export { workpadServiceFactory } from './workpad';
export const pluginServiceProviders: PluginServiceProviders<CanvasPluginServices> = {
@ -42,6 +44,7 @@ export const pluginServiceProviders: PluginServiceProviders<CanvasPluginServices
notify: new PluginServiceProvider(notifyServiceFactory),
platform: new PluginServiceProvider(platformServiceFactory),
reporting: new PluginServiceProvider(reportingServiceFactory),
visualizations: new PluginServiceProvider(visualizationsServiceFactory),
workpad: new PluginServiceProvider(workpadServiceFactory),
};

View file

@ -0,0 +1,19 @@
/*
* 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 { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public';
import { CanvasVisualizationsService } from '../visualizations';
type VisualizationsServiceFactory = PluginServiceFactory<CanvasVisualizationsService>;
const noop = (..._args: any[]): any => {};
export const visualizationsServiceFactory: VisualizationsServiceFactory = () => ({
showNewVisModal: noop,
getByGroup: noop,
getAliases: noop,
});

View file

@ -0,0 +1,14 @@
/*
* 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 { VisualizationsStart } from '../../../../../src/plugins/visualizations/public';
export interface CanvasVisualizationsService {
showNewVisModal: VisualizationsStart['showNewVisModal'];
getByGroup: VisualizationsStart['getByGroup'];
getAliases: VisualizationsStart['getAliases'];
}

View file

@ -40,7 +40,7 @@ export const embeddableReducer = handleActions<
const element = pageWithElement.elements.find((elem) => elem.id === elementId);
if (!element) {
if (!element || element.expression === embeddableExpression) {
return workpadState;
}

View file

@ -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';
@ -30,6 +31,7 @@ import { CanvasRouteHandlerContext, createWorkpadRouteContext } from './workpad_
interface PluginsSetup {
expressions: ExpressionsServerSetup;
embeddable: EmbeddableSetup;
features: FeaturesPluginSetup;
home: HomeServerPluginSetup;
bfetch: BfetchServerSetup;
@ -82,7 +84,12 @@ export class CanvasPlugin implements Plugin {
const kibanaIndex = coreSetup.savedObjects.getKibanaIndex();
registerCanvasUsageCollector(plugins.usageCollection, kibanaIndex);
setupInterpreter(expressionsFork);
setupInterpreter(expressionsFork, {
embeddablePersistableStateService: {
extract: plugins.embeddable.extract,
inject: plugins.embeddable.inject,
},
});
coreSetup.getStartServices().then(([_, depsStart]) => {
const strategy = essqlSearchStrategyProvider();

View file

@ -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));
}

View file

@ -0,0 +1,16 @@
/*
* 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 { TimeRange } from 'src/plugins/data/public';
import { Filter } from '@kbn/es-query';
import { EmbeddableInput as Input } from '../../../../src/plugins/embeddable/common/';
export type EmbeddableInput = Input & {
timeRange?: TimeRange;
filters?: Filter[];
savedObjectId?: string;
};

View file

@ -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
@ -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
>;
/**

View file

@ -9,6 +9,7 @@ export * from '../../../../src/plugins/expressions/common';
export * from './assets';
export * from './canvas';
export * from './elements';
export * from './embeddables';
export * from './filters';
export * from './functions';
export * from './renderers';