diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md index b875b1fce4288..444132024596e 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md @@ -36,6 +36,7 @@ | [isSavedObjectEmbeddableInput(input)](./kibana-plugin-plugins-embeddable-public.issavedobjectembeddableinput.md) | | | [openAddPanelFlyout(options)](./kibana-plugin-plugins-embeddable-public.openaddpanelflyout.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-embeddable-public.plugin.md) | | +| [useEmbeddableFactory({ input, factory, onInputUpdated, })](./kibana-plugin-plugins-embeddable-public.useembeddablefactory.md) | | ## Interfaces diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.useembeddablefactory.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.useembeddablefactory.md new file mode 100644 index 0000000000000..9af20cacc2cee --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.useembeddablefactory.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [useEmbeddableFactory](./kibana-plugin-plugins-embeddable-public.useembeddablefactory.md) + +## useEmbeddableFactory() function + +Signature: + +```typescript +export declare function useEmbeddableFactory({ input, factory, onInputUpdated, }: EmbeddableRendererWithFactory): readonly [ErrorEmbeddable | IEmbeddable | undefined, boolean, string | undefined]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { input, factory, onInputUpdated, } | EmbeddableRendererWithFactory<I> | | + +Returns: + +`readonly [ErrorEmbeddable | IEmbeddable | undefined, boolean, string | undefined]` + diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 25d080dbfd546..80171e1ad2fab 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -69,6 +69,7 @@ export { EmbeddablePackageState, EmbeddableRenderer, EmbeddableRendererProps, + useEmbeddableFactory, } from './lib'; export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './lib/attribute_service'; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx index 457852c48ed77..b919672ad01e3 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.test.tsx @@ -9,14 +9,39 @@ import React from 'react'; import { waitFor } from '@testing-library/dom'; import { render } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; import { HelloWorldEmbeddable, HelloWorldEmbeddableFactoryDefinition, HELLO_WORLD_EMBEDDABLE, } from '../../tests/fixtures'; -import { EmbeddableRenderer } from './embeddable_renderer'; +import { EmbeddableRenderer, useEmbeddableFactory } from './embeddable_renderer'; import { embeddablePluginMock } from '../../mocks'; +describe('useEmbeddableFactory', () => { + it('should update upstream value changes', async () => { + const { setup, doStart } = embeddablePluginMock.createInstance(); + const getFactory = setup.registerEmbeddableFactory( + HELLO_WORLD_EMBEDDABLE, + new HelloWorldEmbeddableFactoryDefinition() + ); + doStart(); + + const { result, waitForNextUpdate } = renderHook(() => + useEmbeddableFactory({ factory: getFactory(), input: { id: 'hello' } }) + ); + + const [, loading] = result.current; + + expect(loading).toBe(true); + + await waitForNextUpdate(); + + const [embeddable] = result.current; + expect(embeddable).toBeDefined(); + }); +}); + describe('', () => { test('Render embeddable', () => { const embeddable = new HelloWorldEmbeddable({ id: 'hello' }); diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.tsx index 153564187d4b5..433b21e92cce5 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_renderer.tsx @@ -28,12 +28,6 @@ interface EmbeddableRendererPropsWithEmbeddable { embeddable: IEmbeddable; } -function isWithEmbeddable( - props: EmbeddableRendererProps -): props is EmbeddableRendererPropsWithEmbeddable { - return 'embeddable' in props; -} - interface EmbeddableRendererWithFactory { input: I; onInputUpdated?: (newInput: I) => void; @@ -46,6 +40,72 @@ function isWithFactory( return 'factory' in props; } +export function useEmbeddableFactory({ + input, + factory, + onInputUpdated, +}: EmbeddableRendererWithFactory) { + const [embeddable, setEmbeddable] = useState | ErrorEmbeddable | undefined>( + undefined + ); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + const latestInput = React.useRef(input); + useEffect(() => { + latestInput.current = input; + }, [input]); + + useEffect(() => { + let canceled = false; + + // keeping track of embeddables created by this component to be able to destroy them + let createdEmbeddableRef: IEmbeddable | ErrorEmbeddable | undefined; + setEmbeddable(undefined); + setLoading(true); + factory + .create(latestInput.current!) + .then((createdEmbeddable) => { + if (canceled) { + if (createdEmbeddable) { + createdEmbeddable.destroy(); + } + } else { + createdEmbeddableRef = createdEmbeddable; + setEmbeddable(createdEmbeddable); + } + }) + .catch((err) => { + if (canceled) return; + setError(err?.message); + }) + .finally(() => { + if (canceled) return; + setLoading(false); + }); + + return () => { + canceled = true; + if (createdEmbeddableRef) { + createdEmbeddableRef.destroy(); + } + }; + }, [factory]); + + useEffect(() => { + if (!embeddable) return; + if (isErrorEmbeddable(embeddable)) return; + if (!onInputUpdated) return; + const sub = embeddable.getInput$().subscribe((newInput) => { + onInputUpdated(newInput); + }); + return () => { + sub.unsubscribe(); + }; + }, [embeddable, onInputUpdated]); + + return [embeddable, loading, error] as const; +} + /** * Helper react component to render an embeddable * Can be used if you have an embeddable object or an embeddable factory @@ -82,72 +142,22 @@ function isWithFactory( export const EmbeddableRenderer = ( props: EmbeddableRendererProps ) => { - const { input, onInputUpdated } = props; - const [embeddable, setEmbeddable] = useState | ErrorEmbeddable | undefined>( - isWithEmbeddable(props) ? props.embeddable : undefined - ); - const [loading, setLoading] = useState(!isWithEmbeddable(props)); - const [error, setError] = useState(); - const latestInput = React.useRef(props.input); - useEffect(() => { - latestInput.current = input; - }, [input]); - - const factoryFromProps = isWithFactory(props) ? props.factory : undefined; - const embeddableFromProps = isWithEmbeddable(props) ? props.embeddable : undefined; - useEffect(() => { - let canceled = false; - if (embeddableFromProps) { - setEmbeddable(embeddableFromProps); - return; - } - - // keeping track of embeddables created by this component to be able to destroy them - let createdEmbeddableRef: IEmbeddable | ErrorEmbeddable | undefined; - if (factoryFromProps) { - setEmbeddable(undefined); - setLoading(true); - factoryFromProps - .create(latestInput.current!) - .then((createdEmbeddable) => { - if (canceled) { - if (createdEmbeddable) { - createdEmbeddable.destroy(); - } - } else { - createdEmbeddableRef = createdEmbeddable; - setEmbeddable(createdEmbeddable); - } - }) - .catch((err) => { - if (canceled) return; - setError(err?.message); - }) - .finally(() => { - if (canceled) return; - setLoading(false); - }); - } - - return () => { - canceled = true; - if (createdEmbeddableRef) { - createdEmbeddableRef.destroy(); - } - }; - }, [factoryFromProps, embeddableFromProps]); - - useEffect(() => { - if (!embeddable) return; - if (isErrorEmbeddable(embeddable)) return; - if (!onInputUpdated) return; - const sub = embeddable.getInput$().subscribe((newInput) => { - onInputUpdated(newInput); - }); - return () => { - sub.unsubscribe(); - }; - }, [embeddable, onInputUpdated]); + if (isWithFactory(props)) { + return ; + } + return ; +}; +// +const EmbeddableByFactory = ({ + factory, + input, + onInputUpdated, +}: EmbeddableRendererWithFactory) => { + const [embeddable, loading, error] = useEmbeddableFactory({ + factory, + input, + onInputUpdated, + }); return ; }; diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 71dfd73e534e7..eede745f31794 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -16,4 +16,8 @@ export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableRoot } from './embeddable_root'; export * from '../../../common/lib/saved_object_embeddable'; -export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; +export { + EmbeddableRenderer, + EmbeddableRendererProps, + useEmbeddableFactory, +} from './embeddable_renderer'; diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 22001608f63ab..1b3e0388e9bb0 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -542,3 +542,40 @@ test('Check when hide header option is true', async () => { const title = findTestSubject(component, `embeddablePanelHeading-HelloAryaStark`); expect(title.length).toBe(0); }); + +test('Should work in minimal way rendering only the inspector action', async () => { + const inspector = inspectorPluginMock.createStartContract(); + inspector.isAvailable = jest.fn(() => true); + + const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, { + getEmbeddableFactory, + } as any); + + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Arya', + lastName: 'Stark', + }); + + const component = mount( + + Promise.resolve([])} + inspector={inspector} + hideHeader={false} + /> + + ); + + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + expect(findTestSubject(component, `embeddablePanelContextMenuOpen`).length).toBe(1); + await nextTick(); + component.update(); + expect(findTestSubject(component, `embeddablePanelAction-openInspector`).length).toBe(1); + const action = findTestSubject(component, `embeddablePanelAction-ACTION_CUSTOMIZE_PANEL`); + expect(action.length).toBe(0); +}); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 8cf2de8c80743..b66950c170d69 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -54,16 +54,20 @@ const removeById = (disabledActions: string[]) => ({ id }: { id: string }) => interface Props { embeddable: IEmbeddable; getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - overlays: CoreStart['overlays']; - notifications: CoreStart['notifications']; - application: CoreStart['application']; - inspector: InspectorStartContract; - SavedObjectFinder: React.ComponentType; + getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories?: EmbeddableStart['getEmbeddableFactories']; + overlays?: CoreStart['overlays']; + notifications?: CoreStart['notifications']; + application?: CoreStart['application']; + inspector?: InspectorStartContract; + SavedObjectFinder?: React.ComponentType; stateTransfer?: EmbeddableStateTransfer; hideHeader?: boolean; + actionPredicate?: (actionId: string) => boolean; reportUiCounter?: UsageCollectionStart['reportUiCounter']; + showShadow?: boolean; + showBadges?: boolean; + showNotifications?: boolean; } interface State { @@ -80,7 +84,11 @@ interface State { errorEmbeddable?: ErrorEmbeddable; } -interface PanelUniversalActions { +interface InspectorPanelAction { + inspectPanel: InspectPanelAction; +} + +interface BasePanelActions { customizePanelTitle: CustomizePanelTitleAction; addPanel: AddPanelAction; inspectPanel: InspectPanelAction; @@ -88,6 +96,15 @@ interface PanelUniversalActions { editPanel: EditPanelAction; } +const emptyObject = {}; +type EmptyObject = typeof emptyObject; + +type PanelUniversalActions = + | BasePanelActions + | InspectorPanelAction + | (BasePanelActions & InspectorPanelAction) + | EmptyObject; + export class EmbeddablePanel extends React.Component { private embeddableRoot: React.RefObject; private parentSubscription?: Subscription; @@ -117,10 +134,15 @@ export class EmbeddablePanel extends React.Component { } private async refreshBadges() { + if (!this.mounted) { + return; + } + if (this.props.showBadges === false) { + return; + } let badges = await this.props.getActions(PANEL_BADGE_TRIGGER, { embeddable: this.props.embeddable, }); - if (!this.mounted) return; const { disabledActions } = this.props.embeddable.getInput(); if (disabledActions) { @@ -135,10 +157,15 @@ export class EmbeddablePanel extends React.Component { } private async refreshNotifications() { + if (!this.mounted) { + return; + } + if (this.props.showNotifications === false) { + return; + } let notifications = await this.props.getActions(PANEL_NOTIFICATION_TRIGGER, { embeddable: this.props.embeddable, }); - if (!this.mounted) return; const { disabledActions } = this.props.embeddable.getInput(); if (disabledActions) { @@ -229,13 +256,18 @@ export class EmbeddablePanel extends React.Component { paddingSize="none" role="figure" aria-labelledby={headerId} + hasShadow={this.props.showShadow} > {!this.props.hideHeader && ( { }; private getUniversalActions = (): PanelUniversalActions => { + let actions = {}; + if (this.props.inspector) { + actions = { + inspectPanel: new InspectPanelAction(this.props.inspector), + }; + } + if ( + !this.props.getEmbeddableFactory || + !this.props.getAllEmbeddableFactories || + !this.props.overlays || + !this.props.notifications || + !this.props.SavedObjectFinder || + !this.props.application + ) { + return actions; + } + const createGetUserData = (overlays: OverlayStart) => async function getUserData(context: { embeddable: IEmbeddable }) { return new Promise<{ title: string | undefined; hideTitle?: boolean }>((resolve) => { @@ -308,6 +357,7 @@ export class EmbeddablePanel extends React.Component { // Universal actions are exposed on the context menu for every embeddable, they bypass the trigger // registry. return { + ...actions, customizePanelTitle: new CustomizePanelTitleAction(createGetUserData(this.props.overlays)), addPanel: new AddPanelAction( this.props.getEmbeddableFactory, @@ -317,7 +367,6 @@ export class EmbeddablePanel extends React.Component { this.props.SavedObjectFinder, this.props.reportUiCounter ), - inspectPanel: new InspectPanelAction(this.props.inspector), removePanel: new RemovePanelAction(), editPanel: new EditPanelAction( this.props.getEmbeddableFactory, @@ -338,9 +387,13 @@ export class EmbeddablePanel extends React.Component { regularActions = regularActions.filter(removeDisabledActions); } - const sortedActions = [...regularActions, ...Object.values(this.state.universalActions)].sort( - sortByOrderField - ); + let sortedActions = regularActions + .concat(Object.values(this.state.universalActions || {}) as Array>) + .sort(sortByOrderField); + + if (this.props.actionPredicate) { + sortedActions = sortedActions.filter(({ id }) => this.props.actionPredicate!(id)); + } return await buildContextMenuForActions({ actions: sortedActions.map((action) => ({ diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 403aa3e3f4c9f..742a2d1909941 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -36,7 +36,7 @@ export interface PanelHeaderProps { embeddable: IEmbeddable; headerId?: string; showPlaceholderTitle?: boolean; - customizeTitle: CustomizePanelTitleAction; + customizeTitle?: CustomizePanelTitleAction; } function renderBadges(badges: Array>, embeddable: IEmbeddable) { @@ -177,7 +177,7 @@ export function PanelHeader({ > {title || placeholderTitle} - ) : ( + ) : customizeTitle ? ( {title || placeholderTitle} - ); + ) : null; } return description ? ( ({ input, factory, onInputUpdated, }: EmbeddableRendererWithFactory): readonly [ErrorEmbeddable | IEmbeddable | undefined, boolean, string | undefined]; + // Warning: (ae-missing-release-tag) "VALUE_CLICK_TRIGGER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/x-pack/examples/embedded_lens_example/kibana.json b/x-pack/examples/embedded_lens_example/kibana.json index 5e4caead90bc3..be38e5bf3e761 100644 --- a/x-pack/examples/embedded_lens_example/kibana.json +++ b/x-pack/examples/embedded_lens_example/kibana.json @@ -8,6 +8,7 @@ "requiredPlugins": [ "lens", "data", + "embeddable", "developerExamples" ], "optionalPlugins": [], diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx index 6a39951ad4958..a13ddbbd79ef0 100644 --- a/x-pack/examples/embedded_lens_example/public/app.tsx +++ b/x-pack/examples/embedded_lens_example/public/app.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/eui'; import { IndexPattern } from 'src/plugins/data/public'; import { CoreStart } from 'kibana/public'; +import { ViewMode } from '../../../../src/plugins/embeddable/public'; import { TypedLensByValueInput, PersistedIndexPatternLayer, @@ -193,6 +194,7 @@ export const App = (props: { { // call back event for on table row click event }} + viewMode={ViewMode.VIEW} /> {isSaveModalVisible && ( & { | LensAttributes<'lnsMetric', MetricState>; }; -export type EmbeddableComponentProps = TypedLensByValueInput | LensByReferenceInput; +export type EmbeddableComponentProps = (TypedLensByValueInput | LensByReferenceInput) & { + withActions?: boolean; +}; + +interface PluginsStartDependencies { + uiActions: UiActionsStart; + embeddable: EmbeddableStart; + inspector: InspectorStartContract; +} -export function getEmbeddableComponent(embeddableStart: EmbeddableStart) { +export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDependencies) { return (props: EmbeddableComponentProps) => { + const { embeddable: embeddableStart, uiActions, inspector } = plugins; const factory = embeddableStart.getEmbeddableFactory('lens')!; - return ; + const input = { ...props }; + const [embeddable, loading, error] = useEmbeddableFactory({ factory, input }); + const hasActions = props.withActions === true; + + if (embeddable && hasActions) { + return ( + } + uiActions={uiActions} + inspector={inspector} + actionPredicate={() => hasActions} + input={input} + /> + ); + } + + return ; }; } + +interface EmbeddablePanelWrapperProps { + embeddable: IEmbeddable; + uiActions: PluginsStartDependencies['uiActions']; + inspector: PluginsStartDependencies['inspector']; + actionPredicate: (id: string) => boolean; + input: EmbeddableComponentProps; +} + +const EmbeddablePanelWrapper: FC = ({ + embeddable, + uiActions, + actionPredicate, + inspector, + input, +}) => { + useEffect(() => { + embeddable.updateInput(input); + }, [embeddable, input]); + + return ( + } + getActions={uiActions.getTriggerCompatibleActions} + inspector={inspector} + actionPredicate={actionPredicate} + showShadow={false} + showBadges={false} + showNotifications={false} + /> + ); +}; diff --git a/x-pack/plugins/lens/public/index.ts b/x-pack/plugins/lens/public/index.ts index 98e0198b9d0fa..a82bc0b9c0d3d 100644 --- a/x-pack/plugins/lens/public/index.ts +++ b/x-pack/plugins/lens/public/index.ts @@ -7,7 +7,7 @@ import { LensPlugin } from './plugin'; -export { +export type { EmbeddableComponentProps, TypedLensByValueInput, } from './editor_frame_service/embeddable/embeddable_component'; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 328bea5def557..ad81413e21345 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -6,6 +6,7 @@ */ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; +import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { UsageCollectionSetup, UsageCollectionStart } from 'src/plugins/usage_collection/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; @@ -81,6 +82,7 @@ export interface LensPluginStartDependencies { savedObjectsTagging?: SavedObjectTaggingPluginStart; presentationUtil: PresentationUtilPluginStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; + inspector: InspectorStartContract; usageCollection?: UsageCollectionStart; } @@ -256,7 +258,7 @@ export class LensPlugin { ); return { - EmbeddableComponent: getEmbeddableComponent(startDependencies.embeddable), + EmbeddableComponent: getEmbeddableComponent(core, startDependencies), SaveModalComponent: getSaveModalComponent(core, startDependencies, this.attributeService!), navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => { // for openInNewTab, we set the time range in url via getEditPath below diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx index 6cdb01a992034..5b1ba3bcc206b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx @@ -78,7 +78,9 @@ export class CustomTimeRangeAction implements Action { const isMarkdown = isVisualizeEmbeddable(embeddable) && (embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'markdown'; - return Boolean(embeddable && hasTimeRange(embeddable) && !isInputControl && !isMarkdown); + return Boolean( + embeddable && embeddable.parent && hasTimeRange(embeddable) && !isInputControl && !isMarkdown + ); } public async execute({ embeddable }: TimeRangeActionContext) {