mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* 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:
parent
e8b538b27e
commit
a4dd96a7b9
60 changed files with 1328 additions and 126 deletions
|
@ -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"
|
||||
|
|
|
@ -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.' },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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];
|
||||
|
|
60
x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts
vendored
Normal file
60
x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts
vendored
Normal 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);
|
||||
});
|
||||
});
|
145
x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts
vendored
Normal file
145
x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts
vendored
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}"`;
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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}"`);
|
||||
|
|
|
@ -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: '',
|
||||
};
|
||||
|
|
|
@ -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}"`);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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}"`);
|
||||
|
|
13
x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts
Normal file
13
x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts
Normal 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());
|
25
x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts
Normal file
25
x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts
Normal 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`,
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"features",
|
||||
"inspector",
|
||||
"presentationUtil",
|
||||
"visualizations",
|
||||
"uiActions",
|
||||
"share"
|
||||
],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,3 +6,5 @@
|
|||
*/
|
||||
|
||||
export { useDownloadWorkpad, useDownloadRenderedWorkpad } from './use_download_workpad';
|
||||
|
||||
export { useIncomingEmbeddable } from './use_incoming_embeddable';
|
||||
|
|
|
@ -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]);
|
||||
};
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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')}
|
||||
/>
|
||||
));
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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())();
|
||||
|
|
|
@ -16,4 +16,5 @@ export type EmbeddablesServiceFactory = KibanaPluginServiceFactory<
|
|||
|
||||
export const embeddablesServiceFactory: EmbeddablesServiceFactory = ({ startPlugins }) => ({
|
||||
getEmbeddableFactories: startPlugins.embeddable.getEmbeddableFactories,
|
||||
getStateTransfer: startPlugins.embeddable.getStateTransfer,
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -14,4 +14,5 @@ const noop = (..._args: any[]): any => {};
|
|||
|
||||
export const embeddablesServiceFactory: EmbeddablesServiceFactory = () => ({
|
||||
getEmbeddableFactories: noop,
|
||||
getStateTransfer: noop,
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
14
x-pack/plugins/canvas/public/services/visualizations.ts
Normal file
14
x-pack/plugins/canvas/public/services/visualizations.ts
Normal 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'];
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
16
x-pack/plugins/canvas/types/embeddables.ts
Normal file
16
x-pack/plugins/canvas/types/embeddables.ts
Normal 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;
|
||||
};
|
|
@ -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
|
||||
>;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue