mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Embeddable Rebuild] Make React embeddables work in Canvas (#179667)
Closes https://github.com/elastic/kibana/issues/179548
## Summary
This PR makes it so that React embeddables will now work in Canvas. It
does so by doing two main things:
1. It ensures that the `ReactEmbeddableRenderer` is used in Canvas when
an embeddable exists in the React embeddable registry - if it does not
exist, it continues to use the legacy embeddable factory's `render`
method.
Since Canvas auto-applies all changes and doesn't have save
functionality like Dashboard does, we must keep track of changes **as
they happen** and update the expression to match the new Embeddable
input - therefore, I had to add a `onAnyStateChange` callback to the
`ReactEmbeddableRenderer` as a backdoor for Canvas. As a bonus to this,
embeddables that **previously** didn't respect inline editing (such as
the Image embeddable) will start to work once they are converted!
3. It adds a new trigger (`ADD_CANVAS_ELEMENT_TRIGGER`) specifically for
registering an embeddable to the **Canvas** add panel menu. This trigger
can be attached to the **same action** that the Dashboard
`ADD_PANEL_TRIGGER` is attached to - this makes it super simple to add
React embeddables to Canvas:
```typescript
uiActions.registerAction<EmbeddableApiContext>({
id: ADD_EMBEDDABLE_ACTION_ID,
isCompatible: async ({ embeddable }) => { ... },
execute: async ({ embeddable }) => { ... },
getDisplayName: () => { ... },
});
// register this action to the Dashboard add panel menu:
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_EMBEDDABLE_ACTION_ID);
// register this action to the Canvas add panel menu:
uiActions.attachAction('ADD_CANVAS_ELEMENT_TRIGGER',
ADD_EMBEDDABLE_ACTION_ID);
```
As a small cleanup, I also replaced some inline embeddable expressions
with `embeddableInputToExpression` - this is because I was originally
missing `| render` at the end of my expression, and I didn't catch it
because the expressions I was comparing it to were all declared inline.
This should help keep things more consistent.
### How to Test
I attached the `ADD_EUI_MARKDOWN_ACTION_ID` action to the new
`ADD_CANVAS_ELEMENT_TRIGGER`, so the new EUI Markdown React embeddable
should show up in the Canvas add panel menu. Ensure that this React
embeddable can be added to a workpad, and make sure it responds to edits
as expected:
d9062949
-9c61-4c9e-8a8e-08042753eab6
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a85ba2b7ee
commit
4cc61b98e1
36 changed files with 473 additions and 104 deletions
|
@ -7,8 +7,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
|
||||
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import { EmbeddableApiContext, apiCanAddNewPanel } from '@kbn/presentation-publishing';
|
||||
import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { ADD_EUI_MARKDOWN_ACTION_ID, EUI_MARKDOWN_ID } from './constants';
|
||||
|
||||
|
@ -21,10 +20,10 @@ export const registerCreateEuiMarkdownAction = (uiActions: UiActionsStart) => {
|
|||
id: ADD_EUI_MARKDOWN_ACTION_ID,
|
||||
getIconType: () => 'editorCodeBlock',
|
||||
isCompatible: async ({ embeddable }) => {
|
||||
return apiIsPresentationContainer(embeddable);
|
||||
return apiCanAddNewPanel(embeddable);
|
||||
},
|
||||
execute: async ({ embeddable }) => {
|
||||
if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError();
|
||||
if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError();
|
||||
embeddable.addNewPanel(
|
||||
{
|
||||
panelType: EUI_MARKDOWN_ID,
|
||||
|
@ -39,4 +38,9 @@ export const registerCreateEuiMarkdownAction = (uiActions: UiActionsStart) => {
|
|||
}),
|
||||
});
|
||||
uiActions.attachAction('ADD_PANEL_TRIGGER', ADD_EUI_MARKDOWN_ACTION_ID);
|
||||
if (uiActions.hasTrigger('ADD_CANVAS_ELEMENT_TRIGGER')) {
|
||||
// Because Canvas is not enabled in Serverless, this trigger might not be registered - only attach
|
||||
// the create action if the Canvas-specific trigger does indeed exist.
|
||||
uiActions.attachAction('ADD_CANVAS_ELEMENT_TRIGGER', ADD_EUI_MARKDOWN_ACTION_ID);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
|
||||
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import { apiCanAddNewPanel, EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin';
|
||||
import { ADD_FIELD_LIST_ACTION_ID, FIELD_LIST_ID } from './constants';
|
||||
|
@ -18,10 +17,10 @@ export const registerCreateFieldListAction = (uiActions: UiActionsPublicStart) =
|
|||
id: ADD_FIELD_LIST_ACTION_ID,
|
||||
getIconType: () => 'indexOpen',
|
||||
isCompatible: async ({ embeddable }) => {
|
||||
return apiIsPresentationContainer(embeddable);
|
||||
return apiCanAddNewPanel(embeddable);
|
||||
},
|
||||
execute: async ({ embeddable }) => {
|
||||
if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError();
|
||||
if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError();
|
||||
embeddable.addNewPanel({
|
||||
panelType: FIELD_LIST_ID,
|
||||
});
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { apiIsPresentationContainer } from '@kbn/presentation-containers';
|
||||
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import { apiCanAddNewPanel, EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import { IncompatibleActionError, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { ADD_SEARCH_ACTION_ID, SEARCH_EMBEDDABLE_ID } from './constants';
|
||||
|
||||
|
@ -19,10 +18,10 @@ export const registerAddSearchPanelAction = (uiActions: UiActionsStart) => {
|
|||
'Demonstrates how to use global filters, global time range, panel time range, and global query state in an embeddable',
|
||||
getIconType: () => 'search',
|
||||
isCompatible: async ({ embeddable }) => {
|
||||
return apiIsPresentationContainer(embeddable);
|
||||
return apiCanAddNewPanel(embeddable);
|
||||
},
|
||||
execute: async ({ embeddable }) => {
|
||||
if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError();
|
||||
if (!apiCanAddNewPanel(embeddable)) throw new IncompatibleActionError();
|
||||
embeddable.addNewPanel(
|
||||
{
|
||||
panelType: SEARCH_EMBEDDABLE_ID,
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
"@kbn/ui-theme",
|
||||
"@kbn/i18n",
|
||||
"@kbn/es-query",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/charts-plugin",
|
||||
|
|
|
@ -15,7 +15,6 @@ export {
|
|||
export {
|
||||
apiIsPresentationContainer,
|
||||
getContainerParentFromAPI,
|
||||
type PanelPackage,
|
||||
type PresentationContainer,
|
||||
} from './interfaces/presentation_container';
|
||||
export { tracksOverlays, type TracksOverlays } from './interfaces/tracks_overlays';
|
||||
|
|
|
@ -6,20 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { apiHasParentApi, PublishesViewMode } from '@kbn/presentation-publishing';
|
||||
import {
|
||||
apiCanAddNewPanel,
|
||||
apiHasParentApi,
|
||||
CanAddNewPanel,
|
||||
PanelPackage,
|
||||
PublishesViewMode,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { PublishesLastSavedState } from './last_saved_state';
|
||||
|
||||
export interface PanelPackage {
|
||||
panelType: string;
|
||||
initialState?: object;
|
||||
}
|
||||
|
||||
export type PresentationContainer = Partial<PublishesViewMode> &
|
||||
PublishesLastSavedState & {
|
||||
addNewPanel: <ApiType extends unknown = unknown>(
|
||||
panel: PanelPackage,
|
||||
displaySuccessMessage?: boolean
|
||||
) => Promise<ApiType | undefined>;
|
||||
PublishesLastSavedState &
|
||||
CanAddNewPanel & {
|
||||
registerPanelApi: <ApiType extends unknown = unknown>(
|
||||
panelId: string,
|
||||
panelApi: ApiType
|
||||
|
@ -32,13 +30,15 @@ export type PresentationContainer = Partial<PublishesViewMode> &
|
|||
};
|
||||
|
||||
export const apiIsPresentationContainer = (api: unknown | null): api is PresentationContainer => {
|
||||
return Boolean(
|
||||
typeof (api as PresentationContainer)?.removePanel === 'function' &&
|
||||
typeof (api as PresentationContainer)?.registerPanelApi === 'function' &&
|
||||
typeof (api as PresentationContainer)?.replacePanel === 'function' &&
|
||||
typeof (api as PresentationContainer)?.addNewPanel === 'function' &&
|
||||
typeof (api as PresentationContainer)?.getChildIds === 'function' &&
|
||||
typeof (api as PresentationContainer)?.getChild === 'function'
|
||||
return (
|
||||
apiCanAddNewPanel(api) &&
|
||||
Boolean(
|
||||
typeof (api as PresentationContainer)?.removePanel === 'function' &&
|
||||
typeof (api as PresentationContainer)?.registerPanelApi === 'function' &&
|
||||
typeof (api as PresentationContainer)?.replacePanel === 'function' &&
|
||||
typeof (api as PresentationContainer)?.getChildIds === 'function' &&
|
||||
typeof (api as PresentationContainer)?.getChild === 'function'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -28,6 +28,11 @@ export {
|
|||
useInheritedViewMode,
|
||||
type CanAccessViewMode,
|
||||
} from './interfaces/can_access_view_mode';
|
||||
export {
|
||||
apiCanAddNewPanel,
|
||||
type CanAddNewPanel,
|
||||
type PanelPackage,
|
||||
} from './interfaces/can_add_new_panel';
|
||||
export { apiHasDisableTriggers, type HasDisableTriggers } from './interfaces/has_disable_triggers';
|
||||
export { hasEditCapabilities, type HasEditCapabilities } from './interfaces/has_edit_capabilities';
|
||||
export { apiHasParentApi, type HasParentApi } from './interfaces/has_parent_api';
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export interface PanelPackage {
|
||||
panelType: string;
|
||||
initialState?: object;
|
||||
}
|
||||
|
||||
/**
|
||||
* This API can add a new panel as a child.
|
||||
*/
|
||||
export interface CanAddNewPanel {
|
||||
addNewPanel: <ApiType extends unknown = unknown>(
|
||||
panel: PanelPackage,
|
||||
displaySuccessMessage?: boolean
|
||||
) => Promise<ApiType | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A type guard which can be used to determine if a given API can add a new panel.
|
||||
*/
|
||||
export const apiCanAddNewPanel = (api: unknown): api is CanAddNewPanel => {
|
||||
return typeof (api as CanAddNewPanel)?.addNewPanel === 'function';
|
||||
};
|
|
@ -30,7 +30,7 @@ import {
|
|||
import type { Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { PanelPackage } from '@kbn/presentation-containers';
|
||||
import { PanelPackage } from '@kbn/presentation-publishing';
|
||||
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
|
||||
import { LocatorPublic } from '@kbn/share-plugin/common';
|
||||
import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen';
|
||||
|
|
|
@ -21,11 +21,8 @@ import {
|
|||
} from 'rxjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
PresentationContainer,
|
||||
PanelPackage,
|
||||
SerializedPanelState,
|
||||
} from '@kbn/presentation-containers';
|
||||
import { PanelPackage } from '@kbn/presentation-publishing';
|
||||
import { PresentationContainer, SerializedPanelState } from '@kbn/presentation-containers';
|
||||
|
||||
import { isSavedObjectEmbeddableInput } from '../../../common/lib/saved_object_embeddable';
|
||||
import { EmbeddableStart } from '../../plugin';
|
||||
|
|
|
@ -12,13 +12,17 @@ import {
|
|||
SerializedPanelState,
|
||||
} from '@kbn/presentation-containers';
|
||||
import { PresentationPanel, PresentationPanelProps } from '@kbn/presentation-panel-plugin/public';
|
||||
import { StateComparators } from '@kbn/presentation-publishing';
|
||||
import { ComparatorDefinition, StateComparators } from '@kbn/presentation-publishing';
|
||||
import React, { useEffect, useImperativeHandle, useMemo, useRef } from 'react';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { debounceTime, skip } from 'rxjs/operators';
|
||||
import { v4 as generateId } from 'uuid';
|
||||
import { getReactEmbeddableFactory } from './react_embeddable_registry';
|
||||
import { startTrackingEmbeddableUnsavedChanges } from './react_embeddable_unsaved_changes';
|
||||
import { DefaultEmbeddableApi, ReactEmbeddableApiRegistration } from './types';
|
||||
|
||||
const ON_STATE_CHANGE_DEBOUNCE = 100;
|
||||
|
||||
/**
|
||||
* Renders a component from the React Embeddable registry into a Presentation Panel.
|
||||
*
|
||||
|
@ -34,6 +38,7 @@ export const ReactEmbeddableRenderer = <
|
|||
parentApi,
|
||||
onApiAvailable,
|
||||
panelProps,
|
||||
onAnyStateChange,
|
||||
}: {
|
||||
maybeId?: string;
|
||||
type: string;
|
||||
|
@ -49,6 +54,11 @@ export const ReactEmbeddableRenderer = <
|
|||
| 'hideHeader'
|
||||
| 'hideInspector'
|
||||
>;
|
||||
/**
|
||||
* This `onAnyStateChange` callback allows the parent to keep track of the state of the embeddable
|
||||
* as it changes. This is **not** expected to change over the lifetime of the component.
|
||||
*/
|
||||
onAnyStateChange?: (state: SerializedPanelState<StateType>) => void;
|
||||
}) => {
|
||||
const cleanupFunction = useRef<(() => void) | null>(null);
|
||||
|
||||
|
@ -68,6 +78,21 @@ export const ReactEmbeddableRenderer = <
|
|||
comparators,
|
||||
factory.deserializeState
|
||||
);
|
||||
|
||||
if (onAnyStateChange) {
|
||||
/**
|
||||
* To avoid unnecessary re-renders, only subscribe to the comparator publishing subjects if
|
||||
* an `onAnyStateChange` callback is provided
|
||||
*/
|
||||
const comparatorDefinitions: Array<ComparatorDefinition<StateType, keyof StateType>> =
|
||||
Object.values(comparators);
|
||||
combineLatest(comparatorDefinitions.map((comparator) => comparator[0]))
|
||||
.pipe(skip(1), debounceTime(ON_STATE_CHANGE_DEBOUNCE))
|
||||
.subscribe(() => {
|
||||
onAnyStateChange(apiRegistration.serializeState());
|
||||
});
|
||||
}
|
||||
|
||||
const fullApi = {
|
||||
...apiRegistration,
|
||||
uuid,
|
||||
|
|
|
@ -39,6 +39,7 @@ const createStartContract = (): Start => {
|
|||
getAction: jest.fn(),
|
||||
hasAction: jest.fn(),
|
||||
getTrigger: jest.fn(),
|
||||
hasTrigger: jest.fn(),
|
||||
getTriggerActions: jest.fn((id: string) => []),
|
||||
getTriggerCompatibleActions: jest.fn((triggerId: string, context: object) =>
|
||||
Promise.resolve([] as Array<Action<object>>)
|
||||
|
|
|
@ -55,6 +55,10 @@ export class UiActionsService {
|
|||
this.triggerToActions.set(trigger.id, []);
|
||||
};
|
||||
|
||||
public readonly hasTrigger = (triggerId: string): boolean => {
|
||||
return Boolean(this.triggers.get(triggerId));
|
||||
};
|
||||
|
||||
public readonly getTrigger = (triggerId: string): TriggerContract => {
|
||||
const trigger = this.triggers.get(triggerId);
|
||||
|
||||
|
|
|
@ -5,25 +5,33 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||
import type { EmbeddableAppContext } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
IEmbeddable,
|
||||
EmbeddableFactory,
|
||||
EmbeddableFactoryNotFoundError,
|
||||
isErrorEmbeddable,
|
||||
EmbeddablePanel,
|
||||
IEmbeddable,
|
||||
isErrorEmbeddable,
|
||||
reactEmbeddableRegistryHasKey,
|
||||
ReactEmbeddableRenderer,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import type { EmbeddableAppContext } from '@kbn/embeddable-plugin/public';
|
||||
import { StartDeps } from '../../plugin';
|
||||
import { EmbeddableExpression } from '../../expression_types/embeddable';
|
||||
import { RendererStrings } from '../../../i18n';
|
||||
import { embeddableInputToExpression } from './embeddable_input_to_expression';
|
||||
import { RendererFactory, EmbeddableInput } from '../../../types';
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||
import React, { FC } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { CANVAS_APP, CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib';
|
||||
import { RendererStrings } from '../../../i18n';
|
||||
import {
|
||||
CanvasContainerApi,
|
||||
EmbeddableInput,
|
||||
RendererFactory,
|
||||
RendererHandlers,
|
||||
} from '../../../types';
|
||||
import { EmbeddableExpression } from '../../expression_types/embeddable';
|
||||
import { StartDeps } from '../../plugin';
|
||||
import { embeddableInputToExpression } from './embeddable_input_to_expression';
|
||||
|
||||
const { embeddable: strings } = RendererStrings;
|
||||
|
||||
|
@ -32,6 +40,39 @@ const embeddablesRegistry: {
|
|||
[key: string]: IEmbeddable | Promise<IEmbeddable>;
|
||||
} = {};
|
||||
|
||||
const renderReactEmbeddable = ({
|
||||
type,
|
||||
uuid,
|
||||
input,
|
||||
container,
|
||||
handlers,
|
||||
}: {
|
||||
type: string;
|
||||
uuid: string;
|
||||
input: EmbeddableInput;
|
||||
container: CanvasContainerApi;
|
||||
handlers: RendererHandlers;
|
||||
}) => {
|
||||
return (
|
||||
<ReactEmbeddableRenderer
|
||||
type={type}
|
||||
maybeId={uuid}
|
||||
parentApi={container as unknown as PresentationContainer}
|
||||
key={`${type}_${uuid}`}
|
||||
state={{ rawState: input }}
|
||||
onAnyStateChange={(newState) => {
|
||||
const newExpression = embeddableInputToExpression(
|
||||
newState.rawState as unknown as EmbeddableInput,
|
||||
type,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
if (newExpression) handlers.onEmbeddableInputChange(newExpression);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
|
||||
const I18nContext = core.i18n.Context;
|
||||
const EmbeddableRenderer: FC<{ embeddable: IEmbeddable }> = ({ embeddable }) => {
|
||||
|
@ -75,20 +116,43 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
|
|||
export const embeddableRendererFactory = (
|
||||
core: CoreStart,
|
||||
plugins: StartDeps
|
||||
): RendererFactory<EmbeddableExpression<EmbeddableInput>> => {
|
||||
): RendererFactory<EmbeddableExpression<EmbeddableInput> & { canvasApi: CanvasContainerApi }> => {
|
||||
const renderEmbeddable = renderEmbeddableFactory(core, plugins);
|
||||
return () => ({
|
||||
name: 'embeddable',
|
||||
displayName: strings.getDisplayName(),
|
||||
help: strings.getHelpDescription(),
|
||||
reuseDomNode: true,
|
||||
render: async (domNode, { input, embeddableType }, handlers) => {
|
||||
render: async (domNode, { input, embeddableType, canvasApi }, handlers) => {
|
||||
const uniqueId = handlers.getElementId();
|
||||
const isByValueEnabled = plugins.presentationUtil.labsService.isProjectEnabled(
|
||||
'labs:canvas:byValueEmbeddable'
|
||||
);
|
||||
|
||||
if (!embeddablesRegistry[uniqueId]) {
|
||||
if (reactEmbeddableRegistryHasKey(embeddableType)) {
|
||||
/**
|
||||
* Prioritize React embeddables
|
||||
*/
|
||||
ReactDOM.render(
|
||||
renderReactEmbeddable({
|
||||
input,
|
||||
handlers,
|
||||
uuid: uniqueId,
|
||||
type: embeddableType,
|
||||
container: canvasApi,
|
||||
}),
|
||||
domNode,
|
||||
() => handlers.done()
|
||||
);
|
||||
|
||||
handlers.onDestroy(() => {
|
||||
handlers.onEmbeddableDestroyed();
|
||||
return ReactDOM.unmountComponentAtNode(domNode);
|
||||
});
|
||||
} else if (!embeddablesRegistry[uniqueId]) {
|
||||
/**
|
||||
* Handle legacy embeddables - embeddable does not exist in registry
|
||||
*/
|
||||
const factory = Array.from(plugins.embeddable.getEmbeddableFactories()).find(
|
||||
(embeddableFactory) => embeddableFactory.type === embeddableType
|
||||
) as EmbeddableFactory<EmbeddableInput>;
|
||||
|
@ -155,6 +219,9 @@ export const embeddableRendererFactory = (
|
|||
return ReactDOM.unmountComponentAtNode(domNode);
|
||||
});
|
||||
} else {
|
||||
/**
|
||||
* Handle legacy embeddables - embeddable already exists in registry
|
||||
*/
|
||||
const embeddable = embeddablesRegistry[uniqueId];
|
||||
|
||||
// updating embeddable input with changes made to expression or filters
|
||||
|
|
|
@ -21,17 +21,27 @@ export const inputToExpressionTypeMap = {
|
|||
/*
|
||||
Take the input from an embeddable and the type of embeddable and convert it into an expression
|
||||
*/
|
||||
export function embeddableInputToExpression(
|
||||
input: EmbeddableInput,
|
||||
export function embeddableInputToExpression<
|
||||
UseGenericEmbeddable extends boolean,
|
||||
ConditionalReturnType = UseGenericEmbeddable extends true ? string : string | undefined
|
||||
>(
|
||||
input: Omit<EmbeddableInput, 'id'>,
|
||||
embeddableType: string,
|
||||
palettes: PaletteRegistry,
|
||||
useGenericEmbeddable?: boolean
|
||||
): string | undefined {
|
||||
palettes?: PaletteRegistry,
|
||||
useGenericEmbeddable?: UseGenericEmbeddable
|
||||
): ConditionalReturnType {
|
||||
// if `useGenericEmbeddable` is `true`, it **always** returns a string
|
||||
if (useGenericEmbeddable) {
|
||||
return genericToExpression(input, embeddableType);
|
||||
return genericToExpression(input, embeddableType) as ConditionalReturnType;
|
||||
}
|
||||
|
||||
// otherwise, depending on if the embeddable type is defined, it might return undefined
|
||||
if (inputToExpressionTypeMap[embeddableType]) {
|
||||
return inputToExpressionTypeMap[embeddableType](input as any, palettes);
|
||||
return inputToExpressionTypeMap[embeddableType](
|
||||
input as any,
|
||||
palettes
|
||||
) as ConditionalReturnType;
|
||||
}
|
||||
|
||||
return undefined as ConditionalReturnType;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,6 @@
|
|||
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}"`;
|
||||
export function toExpression(input: Omit<EmbeddableInput, 'id'>, embeddableType: string): string {
|
||||
return `embeddable config="${encode(input)}" type="${embeddableType}" | render`;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { toExpression as toExpressionString } from '@kbn/interpreter';
|
|||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import type { SavedLensInput } from '../../../functions/external/saved_lens';
|
||||
|
||||
export function toExpression(input: SavedLensInput, palettes: PaletteRegistry): string {
|
||||
export function toExpression(input: SavedLensInput, palettes?: PaletteRegistry): string {
|
||||
const expressionParts = [] as string[];
|
||||
|
||||
expressionParts.push('savedLens');
|
||||
|
@ -26,7 +26,7 @@ export function toExpression(input: SavedLensInput, palettes: PaletteRegistry):
|
|||
);
|
||||
}
|
||||
|
||||
if (input.palette) {
|
||||
if (input.palette && palettes) {
|
||||
expressionParts.push(
|
||||
`palette={${toExpressionString(
|
||||
palettes.get(input.palette.name).toExpression(input.palette.params)
|
||||
|
@ -34,5 +34,5 @@ export function toExpression(input: SavedLensInput, palettes: PaletteRegistry):
|
|||
);
|
||||
}
|
||||
|
||||
return expressionParts.join(' ');
|
||||
return `${expressionParts.join(' ')} | render`;
|
||||
}
|
||||
|
|
|
@ -36,5 +36,5 @@ export function toExpression(input: MapEmbeddableInput & { savedObjectId: string
|
|||
}
|
||||
}
|
||||
|
||||
return expressionParts.join(' ');
|
||||
return `${expressionParts.join(' ')} | render`;
|
||||
}
|
||||
|
|
|
@ -36,5 +36,5 @@ export function toExpression(input: VisualizeInput & { savedObjectId: string }):
|
|||
expressionParts.push(`hideLegend=true`);
|
||||
}
|
||||
|
||||
return expressionParts.join(' ');
|
||||
return `${expressionParts.join(' ')} | render`;
|
||||
}
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
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 { embeddableInputToExpression } from '../../../canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression';
|
||||
import { State } from '../../../types';
|
||||
import { useLabsService } from '../../services';
|
||||
|
||||
|
@ -88,10 +88,12 @@ export const AddEmbeddablePanel: React.FunctionComponent<FlyoutProps> = ({
|
|||
// 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`;
|
||||
partialElement.expression = embeddableInputToExpression(
|
||||
{ savedObjectId: id },
|
||||
type,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
} else if (allowedEmbeddables[type]) {
|
||||
partialElement.expression = allowedEmbeddables[type](id);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { EmbeddableInput, ViewMode } from '@kbn/embeddable-plugin/common';
|
||||
|
||||
import { embeddableInputToExpression } from '../../../canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression';
|
||||
import { CanvasContainerApi } from '../../../types';
|
||||
import { METRIC_TYPE, trackCanvasUiMetric } from '../../lib/ui_metric';
|
||||
// @ts-expect-error unconverted file
|
||||
import { addElement } from '../../state/actions/elements';
|
||||
import { getSelectedPage } from '../../state/selectors/workpad';
|
||||
|
||||
export const useCanvasApi: () => CanvasContainerApi = () => {
|
||||
const selectedPageId = useSelector(getSelectedPage);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const createNewEmbeddable = useCallback(
|
||||
(type: string, embeddableInput: EmbeddableInput) => {
|
||||
if (trackCanvasUiMetric) {
|
||||
trackCanvasUiMetric(METRIC_TYPE.CLICK, type);
|
||||
}
|
||||
if (embeddableInput) {
|
||||
const expression = embeddableInputToExpression(embeddableInput, type, undefined, true);
|
||||
dispatch(addElement(selectedPageId, { expression }));
|
||||
}
|
||||
},
|
||||
[selectedPageId, dispatch]
|
||||
);
|
||||
|
||||
const getCanvasApi = useCallback(() => {
|
||||
return {
|
||||
viewMode: new BehaviorSubject<ViewMode>(ViewMode.EDIT), // always in edit mode
|
||||
addNewPanel: async ({
|
||||
panelType,
|
||||
initialState,
|
||||
}: {
|
||||
panelType: string;
|
||||
initialState: EmbeddableInput;
|
||||
}) => {
|
||||
createNewEmbeddable(panelType, initialState);
|
||||
},
|
||||
} as CanvasContainerApi;
|
||||
}, [createNewEmbeddable]);
|
||||
|
||||
return useMemo(() => getCanvasApi(), [getCanvasApi]);
|
||||
};
|
|
@ -10,7 +10,7 @@ import { useDispatch } from 'react-redux';
|
|||
import { fromExpression } from '@kbn/interpreter';
|
||||
import { ErrorStrings } from '../../../../i18n';
|
||||
import { CANVAS_APP } from '../../../../common/lib';
|
||||
import { decode, encode } from '../../../../common/lib/embeddable_dataurl';
|
||||
import { decode } from '../../../../common/lib/embeddable_dataurl';
|
||||
import { CanvasElement, CanvasPage } from '../../../../types';
|
||||
import { useEmbeddablesService, useLabsService, useNotifyService } from '../../../services';
|
||||
// @ts-expect-error unconverted file
|
||||
|
@ -23,6 +23,7 @@ import {
|
|||
fetchEmbeddableRenderable,
|
||||
} from '../../../state/actions/embeddable';
|
||||
import { clearValue } from '../../../state/actions/resolved_args';
|
||||
import { embeddableInputToExpression } from '../../../../canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression';
|
||||
|
||||
const { actionsElements: strings } = ErrorStrings;
|
||||
|
||||
|
@ -83,9 +84,7 @@ export const useIncomingEmbeddable = (selectedPage: CanvasPage) => {
|
|||
updatedInput = { ...originalInput, ...incomingInput };
|
||||
}
|
||||
|
||||
const expression = `embeddable config="${encode(updatedInput)}"
|
||||
type="${type}"
|
||||
| render`;
|
||||
const expression = embeddableInputToExpression(updatedInput, type, undefined, true);
|
||||
|
||||
dispatch(
|
||||
updateEmbeddableExpression({
|
||||
|
@ -100,9 +99,7 @@ export const useIncomingEmbeddable = (selectedPage: CanvasPage) => {
|
|||
// select new embeddable element
|
||||
dispatch(selectToplevelNodes([embeddableId]));
|
||||
} else {
|
||||
const expression = `embeddable config="${encode(incomingInput)}"
|
||||
type="${type}"
|
||||
| render`;
|
||||
const expression = embeddableInputToExpression(incomingInput, type, undefined, true);
|
||||
dispatch(addElement(selectedPage.id, { expression }));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { useNotifyService } from '../../services';
|
|||
import { RenderToDom } from '../render_to_dom';
|
||||
import { ErrorStrings } from '../../../i18n';
|
||||
import { RendererHandlers } from '../../../types';
|
||||
import { useCanvasApi } from '../hooks/use_canvas_api';
|
||||
|
||||
const { RenderWithFn: strings } = ErrorStrings;
|
||||
|
||||
|
@ -41,6 +42,7 @@ export const RenderWithFn: FC<Props> = ({
|
|||
width,
|
||||
height,
|
||||
}) => {
|
||||
const canvasApi = useCanvasApi();
|
||||
const { error: onError } = useNotifyService();
|
||||
|
||||
const [domNode, setDomNode] = useState<HTMLElement | null>(null);
|
||||
|
@ -88,9 +90,12 @@ export const RenderWithFn: FC<Props> = ({
|
|||
if (!isEqual(handlers.current, incomingHandlers)) {
|
||||
handlers.current = incomingHandlers;
|
||||
}
|
||||
|
||||
await renderFn(renderTarget.current!, config, handlers.current);
|
||||
}, [renderTarget, config, renderFn, incomingHandlers]);
|
||||
/**
|
||||
* we are creating a new react tree when we render the element, so we need to pass the current api as a prop
|
||||
* to the element rather than calling `useCanvasApi` directly
|
||||
*/
|
||||
await renderFn(renderTarget.current!, { ...config, canvasApi }, handlers.current);
|
||||
}, [renderTarget, config, renderFn, incomingHandlers, canvasApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!domNode) {
|
||||
|
|
|
@ -94,10 +94,12 @@ const testVisTypeAliases: VisTypeAlias[] = [
|
|||
|
||||
storiesOf('components/WorkpadHeader/EditorMenu', module).add('default', () => (
|
||||
<EditorMenu
|
||||
addPanelActions={[]}
|
||||
factories={testFactories}
|
||||
promotedVisTypes={testVisTypes}
|
||||
visTypeAliases={testVisTypeAliases}
|
||||
createNewVisType={() => action('createNewVisType')}
|
||||
createNewEmbeddable={() => action('createNewEmbeddable')}
|
||||
createNewEmbeddableFromFactory={() => action('createNewEmbeddableFromFactory')}
|
||||
createNewEmbeddableFromAction={() => action('createNewEmbeddableFromAction')}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -5,16 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
EuiContextMenu,
|
||||
EuiContextMenuPanelItemDescriptor,
|
||||
EuiContextMenuItemIcon,
|
||||
EuiContextMenuPanelItemDescriptor,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EmbeddableFactoryDefinition } from '@kbn/embeddable-plugin/public';
|
||||
import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ToolbarPopover } from '@kbn/shared-ux-button-toolbar';
|
||||
import { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public/actions';
|
||||
import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public';
|
||||
|
||||
import { addCanvasElementTrigger } from '../../../state/triggers/add_canvas_element_trigger';
|
||||
import { useCanvasApi } from '../../hooks/use_canvas_api';
|
||||
|
||||
const strings = {
|
||||
getEditorMenuButtonLabel: () =>
|
||||
|
@ -33,21 +38,30 @@ interface FactoryGroup {
|
|||
|
||||
interface Props {
|
||||
factories: EmbeddableFactoryDefinition[];
|
||||
addPanelActions: Action[];
|
||||
promotedVisTypes: BaseVisType[];
|
||||
visTypeAliases: VisTypeAlias[];
|
||||
createNewVisType: (visType?: BaseVisType | VisTypeAlias) => () => void;
|
||||
createNewEmbeddable: (factory: EmbeddableFactoryDefinition) => () => void;
|
||||
createNewEmbeddableFromFactory: (factory: EmbeddableFactoryDefinition) => () => void;
|
||||
createNewEmbeddableFromAction: (
|
||||
action: Action,
|
||||
context: ActionExecutionContext<object>,
|
||||
closePopover: () => void
|
||||
) => (event: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export const EditorMenu: FC<Props> = ({
|
||||
factories,
|
||||
addPanelActions,
|
||||
promotedVisTypes,
|
||||
visTypeAliases,
|
||||
createNewVisType,
|
||||
createNewEmbeddable,
|
||||
createNewEmbeddableFromAction,
|
||||
createNewEmbeddableFromFactory,
|
||||
}: Props) => {
|
||||
const factoryGroupMap: Record<string, FactoryGroup> = {};
|
||||
const ungroupedFactories: EmbeddableFactoryDefinition[] = [];
|
||||
const canvasApi = useCanvasApi();
|
||||
|
||||
let panelCount = 1;
|
||||
|
||||
|
@ -113,16 +127,37 @@ export const EditorMenu: FC<Props> = ({
|
|||
name: factory.getDisplayName(),
|
||||
icon,
|
||||
toolTipContent,
|
||||
onClick: createNewEmbeddable(factory),
|
||||
onClick: createNewEmbeddableFromFactory(factory),
|
||||
'data-test-subj': `createNew-${factory.type}`,
|
||||
};
|
||||
};
|
||||
|
||||
const editorMenuPanels = [
|
||||
const getAddPanelActionMenuItems = useCallback(
|
||||
(closePopover: () => void) => {
|
||||
return addPanelActions.map((item) => {
|
||||
const context = {
|
||||
embeddable: canvasApi,
|
||||
trigger: addCanvasElementTrigger,
|
||||
};
|
||||
const actionName = item.getDisplayName(context);
|
||||
return {
|
||||
name: actionName,
|
||||
icon: item.getIconType(context),
|
||||
onClick: createNewEmbeddableFromAction(item, context, closePopover),
|
||||
'data-test-subj': `create-action-${actionName}`,
|
||||
toolTipContent: item?.getDisplayNameTooltip?.(context),
|
||||
};
|
||||
});
|
||||
},
|
||||
[addPanelActions, createNewEmbeddableFromAction, canvasApi]
|
||||
);
|
||||
|
||||
const getEditorMenuPanels = (closePopover: () => void) => [
|
||||
{
|
||||
id: 0,
|
||||
items: [
|
||||
...visTypeAliases.map(getVisTypeAliasMenuItem),
|
||||
...getAddPanelActionMenuItems(closePopover),
|
||||
...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({
|
||||
name: appName,
|
||||
icon,
|
||||
|
@ -149,10 +184,10 @@ export const EditorMenu: FC<Props> = ({
|
|||
panelPaddingSize="none"
|
||||
data-test-subj="canvasEditorMenuButton"
|
||||
>
|
||||
{() => (
|
||||
{({ closePopover }: { closePopover: () => void }) => (
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={editorMenuPanels}
|
||||
panels={getEditorMenuPanels(closePopover)}
|
||||
data-test-subj="canvasEditorContextMenu"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -13,12 +13,21 @@ import {
|
|||
EmbeddableFactoryDefinition,
|
||||
EmbeddableInput,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public/actions';
|
||||
|
||||
import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric';
|
||||
import { useEmbeddablesService, useVisualizationsService } from '../../../services';
|
||||
import {
|
||||
useEmbeddablesService,
|
||||
useUiActionsService,
|
||||
useVisualizationsService,
|
||||
} from '../../../services';
|
||||
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';
|
||||
import { embeddableInputToExpression } from '../../../../canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression';
|
||||
import { EmbeddableInput as CanvasEmbeddableInput } from '../../../../canvas_plugin_src/expression_types';
|
||||
import { useCanvasApi } from '../../hooks/use_canvas_api';
|
||||
import { ADD_CANVAS_ELEMENT_TRIGGER } from '../../../state/triggers/add_canvas_element_trigger';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
|
@ -37,6 +46,10 @@ export const EditorMenu: FC<Props> = ({ addElement }) => {
|
|||
const { pathname, search, hash } = useLocation();
|
||||
const stateTransferService = embeddablesService.getStateTransfer();
|
||||
const visualizationsService = useVisualizationsService();
|
||||
const uiActions = useUiActionsService();
|
||||
const canvasApi = useCanvasApi();
|
||||
|
||||
const [addPanelActions, setAddPanelActions] = useState<Array<Action<object>>>([]);
|
||||
|
||||
const embeddableFactories = useMemo(
|
||||
() => (embeddablesService ? Array.from(embeddablesService.getEmbeddableFactories()) : []),
|
||||
|
@ -58,6 +71,21 @@ export const EditorMenu: FC<Props> = ({ addElement }) => {
|
|||
});
|
||||
}, [embeddableFactories]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
async function loadPanelActions() {
|
||||
const registeredActions = await uiActions?.getTriggerCompatibleActions?.(
|
||||
ADD_CANVAS_ELEMENT_TRIGGER,
|
||||
{ embeddable: canvasApi }
|
||||
);
|
||||
if (mounted) setAddPanelActions(registeredActions);
|
||||
}
|
||||
loadPanelActions();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [uiActions, canvasApi]);
|
||||
|
||||
const createNewVisType = useCallback(
|
||||
(visType?: BaseVisType | VisTypeAlias) => () => {
|
||||
let path = '';
|
||||
|
@ -93,11 +121,12 @@ export const EditorMenu: FC<Props> = ({ addElement }) => {
|
|||
[stateTransferService, pathname, search, hash]
|
||||
);
|
||||
|
||||
const createNewEmbeddable = useCallback(
|
||||
const createNewEmbeddableFromFactory = useCallback(
|
||||
(factory: EmbeddableFactoryDefinition) => async () => {
|
||||
if (trackCanvasUiMetric) {
|
||||
trackCanvasUiMetric(METRIC_TYPE.CLICK, factory.type);
|
||||
}
|
||||
|
||||
let embeddableInput;
|
||||
if (factory.getExplicitInput) {
|
||||
embeddableInput = await factory.getExplicitInput();
|
||||
|
@ -107,17 +136,37 @@ export const EditorMenu: FC<Props> = ({ addElement }) => {
|
|||
}
|
||||
|
||||
if (embeddableInput) {
|
||||
const config = encode(embeddableInput);
|
||||
const expression = `embeddable config="${config}"
|
||||
type="${factory.type}"
|
||||
| render`;
|
||||
|
||||
const expression = embeddableInputToExpression(
|
||||
embeddableInput as CanvasEmbeddableInput,
|
||||
factory.type,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
addElement({ expression });
|
||||
}
|
||||
},
|
||||
[addElement]
|
||||
);
|
||||
|
||||
const createNewEmbeddableFromAction = useCallback(
|
||||
(action: Action, context: ActionExecutionContext<object>, closePopover: () => void) =>
|
||||
(event: React.MouseEvent) => {
|
||||
closePopover();
|
||||
if (event.currentTarget instanceof HTMLAnchorElement) {
|
||||
if (
|
||||
!event.defaultPrevented && // onClick prevented default
|
||||
event.button === 0 &&
|
||||
(!event.currentTarget.target || event.currentTarget.target === '_self') &&
|
||||
!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
action.execute(context);
|
||||
}
|
||||
} else action.execute(context);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getVisTypesByGroup = (group: VisGroups): BaseVisType[] =>
|
||||
visualizationsService
|
||||
.getByGroup(group)
|
||||
|
@ -156,9 +205,11 @@ export const EditorMenu: FC<Props> = ({ addElement }) => {
|
|||
return (
|
||||
<Component
|
||||
createNewVisType={createNewVisType}
|
||||
createNewEmbeddable={createNewEmbeddable}
|
||||
createNewEmbeddableFromFactory={createNewEmbeddableFromFactory}
|
||||
createNewEmbeddableFromAction={createNewEmbeddableFromAction}
|
||||
promotedVisTypes={promotedVisTypes}
|
||||
factories={factories}
|
||||
addPanelActions={addPanelActions}
|
||||
visTypeAliases={visTypeAliases}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -23,7 +23,7 @@ import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
|
|||
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { ExpressionsSetup, ExpressionsStart } from '@kbn/expressions-plugin/public';
|
||||
import { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import { Start as InspectorStart } from '@kbn/inspector-plugin/public';
|
||||
|
@ -38,6 +38,7 @@ import { getSessionStorage } from './lib/storage';
|
|||
import { initLoadingIndicator } from './lib/loading_indicator';
|
||||
import { getPluginApi, CanvasApi } from './plugin_api';
|
||||
import { setupExpressions } from './setup_expressions';
|
||||
import { addCanvasElementTrigger } from './state/triggers/add_canvas_element_trigger';
|
||||
|
||||
export type { CoreStart, CoreSetup };
|
||||
|
||||
|
@ -54,6 +55,7 @@ export interface CanvasSetupDeps {
|
|||
usageCollection?: UsageCollectionSetup;
|
||||
bfetch: BfetchPublicSetup;
|
||||
charts: ChartsPluginSetup;
|
||||
uiActions: UiActionsSetup;
|
||||
}
|
||||
|
||||
export interface CanvasStartDeps {
|
||||
|
@ -182,6 +184,8 @@ export class CanvasPlugin
|
|||
return transitions;
|
||||
});
|
||||
|
||||
setupPlugins.uiActions.registerTrigger(addCanvasElementTrigger);
|
||||
|
||||
return {
|
||||
...canvasApi,
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@ import { CanvasPlatformService } from './platform';
|
|||
import { CanvasReportingService } from './reporting';
|
||||
import { CanvasVisualizationsService } from './visualizations';
|
||||
import { CanvasWorkpadService } from './workpad';
|
||||
import { CanvasUiActionsService } from './ui_actions';
|
||||
|
||||
export interface CanvasPluginServices {
|
||||
customElement: CanvasCustomElementService;
|
||||
|
@ -35,6 +36,7 @@ export interface CanvasPluginServices {
|
|||
reporting: CanvasReportingService;
|
||||
visualizations: CanvasVisualizationsService;
|
||||
workpad: CanvasWorkpadService;
|
||||
uiActions: CanvasUiActionsService;
|
||||
}
|
||||
|
||||
export const pluginServices = new PluginServices<CanvasPluginServices>();
|
||||
|
@ -55,3 +57,4 @@ export const useReportingService = () => (() => pluginServices.getHooks().report
|
|||
export const useVisualizationsService = () =>
|
||||
(() => pluginServices.getHooks().visualizations.useService())();
|
||||
export const useWorkpadService = () => (() => pluginServices.getHooks().workpad.useService())();
|
||||
export const useUiActionsService = () => (() => pluginServices.getHooks().uiActions.useService())();
|
||||
|
|
|
@ -26,6 +26,7 @@ import { reportingServiceFactory } from './reporting';
|
|||
import { visualizationsServiceFactory } from './visualizations';
|
||||
import { workpadServiceFactory } from './workpad';
|
||||
import { filtersServiceFactory } from './filters';
|
||||
import { uiActionsServiceFactory } from './ui_actions';
|
||||
|
||||
export { customElementServiceFactory } from './custom_element';
|
||||
export { dataViewsServiceFactory } from './data_views';
|
||||
|
@ -38,6 +39,7 @@ export { platformServiceFactory } from './platform';
|
|||
export { reportingServiceFactory } from './reporting';
|
||||
export { visualizationsServiceFactory } from './visualizations';
|
||||
export { workpadServiceFactory } from './workpad';
|
||||
export { uiActionsServiceFactory } from './ui_actions';
|
||||
|
||||
export const pluginServiceProviders: PluginServiceProviders<
|
||||
CanvasPluginServices,
|
||||
|
@ -55,6 +57,7 @@ export const pluginServiceProviders: PluginServiceProviders<
|
|||
reporting: new PluginServiceProvider(reportingServiceFactory),
|
||||
visualizations: new PluginServiceProvider(visualizationsServiceFactory),
|
||||
workpad: new PluginServiceProvider(workpadServiceFactory),
|
||||
uiActions: new PluginServiceProvider(uiActionsServiceFactory),
|
||||
};
|
||||
|
||||
export const pluginServiceRegistry = new PluginServiceRegistry<
|
||||
|
|
19
x-pack/plugins/canvas/public/services/kibana/ui_actions.ts
Normal file
19
x-pack/plugins/canvas/public/services/kibana/ui_actions.ts
Normal 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 { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public';
|
||||
import { CanvasStartDeps } from '../../plugin';
|
||||
import { CanvasUiActionsService } from '../ui_actions';
|
||||
|
||||
export type UiActionsServiceFactory = KibanaPluginServiceFactory<
|
||||
CanvasUiActionsService,
|
||||
CanvasStartDeps
|
||||
>;
|
||||
|
||||
export const uiActionsServiceFactory: UiActionsServiceFactory = ({ startPlugins }) => ({
|
||||
getTriggerCompatibleActions: startPlugins.uiActions.getTriggerCompatibleActions,
|
||||
});
|
|
@ -26,6 +26,7 @@ import { reportingServiceFactory } from './reporting';
|
|||
import { visualizationsServiceFactory } from './visualizations';
|
||||
import { workpadServiceFactory } from './workpad';
|
||||
import { filtersServiceFactory } from './filters';
|
||||
import { uiActionsServiceFactory } from './ui_actions';
|
||||
|
||||
export { customElementServiceFactory } from './custom_element';
|
||||
export { dataViewsServiceFactory } from './data_views';
|
||||
|
@ -52,6 +53,7 @@ export const pluginServiceProviders: PluginServiceProviders<CanvasPluginServices
|
|||
reporting: new PluginServiceProvider(reportingServiceFactory),
|
||||
visualizations: new PluginServiceProvider(visualizationsServiceFactory),
|
||||
workpad: new PluginServiceProvider(workpadServiceFactory),
|
||||
uiActions: new PluginServiceProvider(uiActionsServiceFactory),
|
||||
};
|
||||
|
||||
export const pluginServiceRegistry = new PluginServiceRegistry<CanvasPluginServices>(
|
||||
|
|
17
x-pack/plugins/canvas/public/services/stubs/ui_actions.ts
Normal file
17
x-pack/plugins/canvas/public/services/stubs/ui_actions.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 '@kbn/presentation-util-plugin/public';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { CanvasUiActionsService } from '../ui_actions';
|
||||
|
||||
type UiActionsServiceFactory = PluginServiceFactory<CanvasUiActionsService>;
|
||||
|
||||
export const uiActionsServiceFactory: UiActionsServiceFactory = () => {
|
||||
const pluginMock = uiActionsPluginMock.createStartContract();
|
||||
return { getTriggerCompatibleActions: pluginMock.getTriggerCompatibleActions };
|
||||
};
|
12
x-pack/plugins/canvas/public/services/ui_actions.ts
Normal file
12
x-pack/plugins/canvas/public/services/ui_actions.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
export interface CanvasUiActionsService {
|
||||
getTriggerCompatibleActions: UiActionsStart['getTriggerCompatibleActions'];
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 type { Trigger } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
export const ADD_CANVAS_ELEMENT_TRIGGER = 'ADD_CANVAS_ELEMENT_TRIGGER';
|
||||
export const addCanvasElementTrigger: Trigger = {
|
||||
id: ADD_CANVAS_ELEMENT_TRIGGER,
|
||||
title: i18n.translate('xpack.canvas.addCanvasElementTrigger.title', {
|
||||
defaultMessage: 'Add panel menu',
|
||||
}),
|
||||
description: i18n.translate('xpack.canvas.addCanvasElementTrigger.description', {
|
||||
defaultMessage: 'A new action will appear in the Canvas add panel menu',
|
||||
}),
|
||||
};
|
|
@ -88,6 +88,8 @@
|
|||
"@kbn/reporting-export-types-pdf-common",
|
||||
"@kbn/code-editor",
|
||||
"@kbn/shared-ux-markdown",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/presentation-publishing",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -8,9 +8,12 @@
|
|||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { EmbeddableInput as Input } from '@kbn/embeddable-plugin/common';
|
||||
import { CanAddNewPanel, PublishesViewMode } from '@kbn/presentation-publishing';
|
||||
|
||||
export type EmbeddableInput = Input & {
|
||||
timeRange?: TimeRange;
|
||||
filters?: Filter[];
|
||||
savedObjectId?: string;
|
||||
};
|
||||
|
||||
export type CanvasContainerApi = PublishesViewMode & CanAddNewPanel;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue