diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx index 6f9e0b5892a89..4e2aea60ad758 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx @@ -80,12 +80,6 @@ export function DashboardEditingToolbar({ isDisabled }: { isDisabled?: boolean } [dashboardApi] ); - /** - * embeddableFactory: Required, you can get the factory from embeddableStart.getEmbeddableFactory() - * initialInput: Optional, use it in case you want to pass your own input to the factory - * dismissNotification: Optional, if not passed a toast will appear in the dashboard - */ - const controlGroupApi = useStateFromPublishingSubject(dashboardApi.controlGroupApi$); const extraButtons = [ , diff --git a/src/plugins/dashboard/public/dashboard_container/types.ts b/src/plugins/dashboard/public/dashboard_container/types.ts index 631d9f66499c7..2eff03bf10913 100644 --- a/src/plugins/dashboard/public/dashboard_container/types.ts +++ b/src/plugins/dashboard/public/dashboard_container/types.ts @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { ContainerOutput } from '@kbn/embeddable-plugin/public'; import { SerializableRecord } from '@kbn/utility-types'; import { ControlGroupRuntimeState } from '@kbn/controls-plugin/public'; @@ -34,10 +33,6 @@ export interface DashboardRenderPerformanceStats { panelsRenderStartTime: number; } -export interface DashboardContainerOutput extends ContainerOutput { - usedDataViewIds?: string[]; -} - export type DashboardLoadedEventStatus = 'done' | 'error'; export interface DashboardLoadedEventMeta { diff --git a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx b/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx deleted file mode 100644 index 8747c78eb3bae..0000000000000 --- a/src/plugins/embeddable/public/embeddable_panel/embeddable_panel.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { css } from '@emotion/react'; -import { PresentationPanel } from '@kbn/presentation-panel-plugin/public'; -import { PanelCompatibleComponent } from '@kbn/presentation-panel-plugin/public/panel_component/types'; -import { isPromise } from '@kbn/std'; -import React, { ReactNode, useEffect, useImperativeHandle, useMemo, useState, useRef } from 'react'; -import { untilPluginStartServicesReady } from '../kibana_services'; -import { EmbeddablePanelProps } from './types'; - -const getComponentFromEmbeddable = async ( - embeddable: EmbeddablePanelProps['embeddable'], - isMounted: () => boolean -): Promise => { - const startServicesPromise = untilPluginStartServicesReady(); - const embeddablePromise = - typeof embeddable === 'function' ? embeddable() : Promise.resolve(embeddable); - const [, unwrappedEmbeddable] = await Promise.all([startServicesPromise, embeddablePromise]); - if (!isMounted()) { - return null; - } - if (unwrappedEmbeddable.parent) { - await unwrappedEmbeddable.parent.untilEmbeddableLoaded(unwrappedEmbeddable.id); - } - - return React.forwardRef((props, apiRef) => { - const [node, setNode] = useState(); - const embeddableRoot: React.RefObject = useMemo(() => React.createRef(), []); - - // Render legacy embeddable into ref, and destroy on unmount. - useEffect(() => { - if (!embeddableRoot.current) return; - const nextNode = unwrappedEmbeddable.render(embeddableRoot.current) ?? undefined; - if (isPromise(nextNode)) { - nextNode.then((resolved) => setNode(resolved)); - } else { - setNode(nextNode); - } - return () => { - unwrappedEmbeddable.destroy(); - }; - }, [embeddableRoot]); - - useImperativeHandle(apiRef, () => unwrappedEmbeddable); - - return ( -
- {node} -
- ); - }); -}; - -/** - * @deprecated - * Loads and renders a legacy embeddable. - * - * Ancestry chain must use 'key' attribute to reset DOM and state when embeddable changes - * For example - */ -export const EmbeddablePanel = (props: EmbeddablePanelProps) => { - // can not use useMountedState - // 1. useMountedState defaults mountedRef to false and sets mountedRef to true in useEffect - // 2. embeddable can be an object or a function that returns a promise - // 3. when embeddable is an object, Promise.resolve(embeddable) returns before - // useMountedState useEffect is called and thus isMounted() returns false when component has not been unmounted - const mountedRef = useRef(true); - useEffect(() => { - return () => { - mountedRef.current = false; - }; - }, []); - const isMounted = () => { - return mountedRef.current; - }; - const { embeddable, ...passThroughProps } = props; - const componentPromise = useMemo( - () => getComponentFromEmbeddable(embeddable, isMounted), - // Ancestry chain is expected to use 'key' attribute to reset DOM and state - // when embeddable needs to be re-loaded - // empty array is consistent with PresentationPanel useAsync dependency check - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - return ; -}; diff --git a/src/plugins/embeddable/public/embeddable_panel/index.ts b/src/plugins/embeddable/public/embeddable_panel/index.ts deleted file mode 100644 index 72f6a47b6af38..0000000000000 --- a/src/plugins/embeddable/public/embeddable_panel/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { EmbeddablePanel } from './embeddable_panel'; diff --git a/src/plugins/embeddable/public/embeddable_panel/types.ts b/src/plugins/embeddable/public/embeddable_panel/types.ts deleted file mode 100644 index 385dcb96e33c8..0000000000000 --- a/src/plugins/embeddable/public/embeddable_panel/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PresentationPanelProps } from '@kbn/presentation-panel-plugin/public'; -import { MaybePromise } from '@kbn/utility-types'; -import { ReactNode } from 'react'; -import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../lib'; - -export type LegacyCompatibleEmbeddable = IEmbeddable< - EmbeddableInput, - EmbeddableOutput, - MaybePromise ->; - -export type EmbeddablePanelProps = Omit & { - embeddable: LegacyCompatibleEmbeddable | (() => Promise); -}; - -export type UnwrappedEmbeddablePanelProps = Omit & { - embeddable: LegacyCompatibleEmbeddable; -}; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 22d845be9c933..c3bb2a796b286 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -12,11 +12,9 @@ import { EmbeddablePublicPlugin } from './plugin'; export { useAddFromLibraryTypes } from './add_from_library/registry'; export { openAddFromLibraryFlyout } from './add_from_library/open_add_from_library_flyout'; -export { EmbeddablePanel } from './embeddable_panel'; export { cellValueTrigger, CELL_VALUE_TRIGGER, - Container, contextMenuTrigger, CONTEXT_MENU_TRIGGER, defaultEmbeddableFactoryProvider, @@ -58,9 +56,6 @@ export type { Adapters, CellValueContext, ChartActionContext, - ContainerInput, - ContainerOutput, - EmbeddableContainerSettings, EmbeddableContext, EmbeddableEditorState, EmbeddableFactory, @@ -70,11 +65,9 @@ export type { EmbeddableOutput, EmbeddablePackageState, FilterableEmbeddable, - IContainer, IEmbeddable, MultiValueClickContext, OutputSpec, - PanelState, PropertySpec, RangeSelectContext, ReferenceOrValueEmbeddable, diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts deleted file mode 100644 index a160c469f05c3..0000000000000 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ /dev/null @@ -1,615 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import deepEqual from 'fast-deep-equal'; -import { isEqual, xor } from 'lodash'; -import { BehaviorSubject, EMPTY, merge, Subscription } from 'rxjs'; -import { - catchError, - combineLatestWith, - distinctUntilChanged, - map, - mergeMap, - pairwise, - switchMap, - take, -} from 'rxjs'; -import { v4 as uuidv4 } from 'uuid'; - -import { PanelPackage } from '@kbn/presentation-containers'; -import { PresentationContainer } from '@kbn/presentation-containers'; - -import { isSavedObjectEmbeddableInput } from '../../../common/lib/saved_object_embeddable'; -import { EmbeddableStart } from '../../plugin'; -import { - Embeddable, - EmbeddableFactory, - EmbeddableInput, - EmbeddableOutput, - ErrorEmbeddable, - IEmbeddable, - isErrorEmbeddable, -} from '../embeddables'; -import { EmbeddableFactoryNotFoundError, PanelNotFoundError } from '../errors'; -import { - ContainerInput, - ContainerOutput, - EmbeddableContainerSettings, - IContainer, - PanelState, -} from './i_container'; - -const getKeys = (o: T): Array => Object.keys(o) as Array; - -export abstract class Container< - TChildInput extends Partial = {}, - TContainerInput extends ContainerInput = ContainerInput, - TContainerOutput extends ContainerOutput = ContainerOutput - > - extends Embeddable - implements IContainer, PresentationContainer -{ - public readonly isContainer: boolean = true; - - public children$: BehaviorSubject<{ [key: string]: unknown }> = new BehaviorSubject<{ - [key: string]: unknown; - }>({}); - - private subscription: Subscription | undefined; - private readonly anyChildOutputChange$; - - constructor( - input: TContainerInput, - output: TContainerOutput, - protected readonly getFactory: EmbeddableStart['getEmbeddableFactory'], - parent?: IContainer, - settings?: EmbeddableContainerSettings - ) { - super(input, output, parent); - this.getFactory = getFactory; // Currently required for using in storybook due to https://github.com/storybookjs/storybook/issues/13834 - - // if there is no special initialization logic, we can immediately start updating children on input updates. - const awaitingInitialize = Boolean( - settings?.initializeSequentially || settings?.childIdInitializeOrder - ); - - const init$ = this.getInput$().pipe( - take(1), - mergeMap(async (currentInput) => { - if (settings?.untilContainerInitialized) { - await settings.untilContainerInitialized(); - } - const initPromise = this.initializeChildEmbeddables(currentInput, settings); - if (awaitingInitialize) await initPromise; - }) - ); - - // on all subsequent input changes, diff and update children on changes. - const update$ = this.getInput$() - // At each update event, get both the previous and current state. - .pipe(pairwise()); - - this.subscription = init$ - .pipe(combineLatestWith(update$)) - .subscribe(([_, [{ panels: prevPanels }, { panels: currentPanels }]]) => { - this.maybeUpdateChildren(currentPanels, prevPanels); - }); - - this.anyChildOutputChange$ = this.getOutput$().pipe( - map(() => this.getChildIds()), - distinctUntilChanged(deepEqual), - - // children may change, so make sure we subscribe/unsubscribe with switchMap - switchMap((newChildIds: string[]) => - merge( - ...newChildIds.map((childId) => - this.getChild(childId) - .getOutput$() - .pipe( - // Embeddables often throw errors into their output streams. - catchError(() => EMPTY), - map(() => childId) - ) - ) - ) - ) - ); - } - - public getPanelCount() { - return Object.keys(this.getInput().panels).length; - } - - public removePanel(id: string) { - this.removeEmbeddable(id); - } - - public async addNewPanel( - panelPackage: PanelPackage - ): Promise { - const newEmbeddable = await this.addNewEmbeddable( - panelPackage.panelType, - panelPackage.initialState as Partial - ); - return newEmbeddable as ApiType; - } - - public async replacePanel(idToRemove: string, { panelType, initialState }: PanelPackage) { - return await this.replaceEmbeddable( - idToRemove, - initialState as Partial, - panelType, - true - ); - } - - public setChildLoaded(embeddable: IEmbeddable) { - // make sure the panel wasn't removed in the mean time, since the embeddable creation is async - if (!this.input.panels[embeddable.id]) { - embeddable.destroy(); - return; - } - - const currentChildren = this.children$.value; - this.children$.next({ - ...currentChildren, - [embeddable.id]: embeddable, - }); - this.updateOutput({ - embeddableLoaded: { - ...this.output.embeddableLoaded, - [embeddable.id]: true, - }, - } as Partial); - } - - public updateInputForChild( - id: string, - changes: Partial - ) { - if (!this.input.panels[id]) { - throw new PanelNotFoundError(); - } - const panels = { - panels: { - ...this.input.panels, - [id]: { - ...this.input.panels[id], - explicitInput: { - ...this.input.panels[id].explicitInput, - ...changes, - }, - }, - }, - }; - this.updateInput(panels as Partial); - } - - public reload() { - for (const child of Object.values(this.children$.value)) { - (child as IEmbeddable)?.reload?.(); - } - } - - public async addNewEmbeddable< - EEI extends EmbeddableInput = EmbeddableInput, - EEO extends EmbeddableOutput = EmbeddableOutput, - E extends IEmbeddable = IEmbeddable - >(type: string, explicitInput: Partial, attributes?: unknown): Promise { - const factory = this.getFactory(type) as EmbeddableFactory | undefined; - - if (!factory) { - throw new EmbeddableFactoryNotFoundError(type); - } - - const { newPanel, otherPanels } = this.createNewPanelState( - factory, - explicitInput, - attributes - ); - - return this.createAndSaveEmbeddable(type, newPanel, otherPanels); - } - - public async replaceEmbeddable< - EEI extends EmbeddableInput = EmbeddableInput, - EEO extends EmbeddableOutput = EmbeddableOutput, - E extends IEmbeddable = IEmbeddable - >( - id: string, - newExplicitInput: Partial, - newType?: string, - generateNewId?: boolean - ): Promise { - if (!this.input.panels[id]) { - throw new PanelNotFoundError(); - } - - if (newType && newType !== this.input.panels[id].type) { - const factory = this.getFactory(newType) as EmbeddableFactory | undefined; - if (!factory) { - throw new EmbeddableFactoryNotFoundError(newType); - } - } - - const panels = { ...this.input.panels }; - const oldPanel = panels[id]; - - if (generateNewId) { - delete panels[id]; - id = uuidv4(); - } - this.updateInput({ - panels: { - ...panels, - [id]: { - ...oldPanel, - explicitInput: { ...newExplicitInput, id }, - type: newType ?? oldPanel.type, - }, - }, - } as Partial); - - await this.untilEmbeddableLoaded(id); - return id; - } - - public removeEmbeddable(embeddableId: string) { - // Just a shortcut for removing the panel from input state, all internal state will get cleaned up naturally - // by the listener. - const panels = this.onRemoveEmbeddable(embeddableId); - this.updateInput({ panels } as Partial); - } - - /** - * Control the panels that are pushed to the input stream when an embeddable is - * removed. This can be used if removing one embeddable has knock-on effects, like - * re-ordering embeddables that come after it. - */ - protected onRemoveEmbeddable(embeddableId: string): ContainerInput['panels'] { - const panels = { ...this.input.panels }; - delete panels[embeddableId]; - return panels; - } - - public getChildIds(): string[] { - return Object.keys(this.children$.value); - } - - public getChild(id: string): E { - return this.children$.value[id] as E; - } - - public getInputForChild( - embeddableId: string - ): TEmbeddableInput { - const containerInput: TChildInput = this.getInheritedInput(embeddableId); - const panelState = this.getPanelState(embeddableId); - - const explicitInput = panelState.explicitInput; - const explicitFiltered: { [key: string]: unknown } = {}; - - const keys = getKeys(panelState.explicitInput); - - // If explicit input for a particular value is undefined, and container has that input defined, - // we will use the inherited container input. This way children can set a value to undefined in order - // to default back to inherited input. However, if the particular value is not part of the container, then - // the caller may be trying to explicitly tell the child to clear out a given value, so in that case, we want - // to pass it along. - keys.forEach((key) => { - if (explicitInput[key] === undefined && containerInput[key] !== undefined) { - return; - } - explicitFiltered[key] = explicitInput[key]; - }); - - return { - ...containerInput, - ...explicitFiltered, - // Typescript has difficulties with inferring this type but it is accurate with all - // tests I tried. Could probably be revisted with future releases of TS to see if - // it can accurately infer the type. - } as unknown as TEmbeddableInput; - } - - public getAnyChildOutputChange$() { - return this.anyChildOutputChange$; - } - - public destroy() { - super.destroy(); - for (const child of Object.values(this.children$.value)) { - (child as IEmbeddable)?.destroy?.(); - } - this.subscription?.unsubscribe(); - } - - public async untilEmbeddableLoaded( - id: string - ): Promise { - if (!this.input.panels[id]) { - throw new PanelNotFoundError(); - } - - if (this.output.embeddableLoaded[id]) { - return this.children$.value[id] as TEmbeddable; - } - - return new Promise((resolve, reject) => { - const subscription = merge(this.getOutput$(), this.getInput$()).subscribe(() => { - if (this.output.embeddableLoaded[id]) { - subscription.unsubscribe(); - resolve(this.children$.value[id] as TEmbeddable); - } - - // If we hit this, the panel was removed before the embeddable finished loading. - if (this.input.panels[id] === undefined) { - subscription.unsubscribe(); - // @ts-expect-error undefined in not assignable to TEmbeddable | ErrorEmbeddable - resolve(undefined); - } - }); - }); - } - - public async untilReactEmbeddableLoaded(id: string): Promise { - if (!this.input.panels[id]) { - throw new PanelNotFoundError(); - } - - if (this.children$.value[id]) { - return this.children$.value[id] as ApiType; - } - - return new Promise((resolve, reject) => { - const subscription = merge(this.children$, this.getInput$()).subscribe(() => { - if (this.children$.value[id]) { - subscription.unsubscribe(); - resolve(this.children$.value[id] as ApiType); - } - - // If we hit this, the panel was removed before the embeddable finished loading. - if (this.input.panels[id] === undefined) { - subscription.unsubscribe(); - resolve(undefined); - } - }); - }); - } - - public async getExplicitInputIsEqual(lastInput: TContainerInput) { - const { panels: lastPanels, ...restOfLastInput } = lastInput; - const { panels: currentPanels, ...restOfCurrentInput } = this.getExplicitInput(); - const otherInputIsEqual = isEqual(restOfLastInput, restOfCurrentInput); - if (!otherInputIsEqual) return false; - - const embeddableIdsA = Object.keys(lastPanels); - const embeddableIdsB = Object.keys(currentPanels); - if ( - embeddableIdsA.length !== embeddableIdsB.length || - xor(embeddableIdsA, embeddableIdsB).length > 0 - ) { - return false; - } - // embeddable ids are equal so let's compare individual panels. - for (const id of embeddableIdsA) { - const currentEmbeddable = await this.untilEmbeddableLoaded(id); - const lastPanelInput = lastPanels[id].explicitInput; - if (isErrorEmbeddable(currentEmbeddable)) continue; - if (!(await currentEmbeddable.getExplicitInputIsEqual(lastPanelInput))) { - return false; - } - } - return true; - } - - protected createNewPanelState< - TEmbeddableInput extends EmbeddableInput, - TEmbeddable extends IEmbeddable - >( - factory: EmbeddableFactory, - partial: Partial = {}, - attributes?: unknown - ): { newPanel: PanelState; otherPanels: TContainerInput['panels'] } { - const embeddableId = partial.id || uuidv4(); - - const explicitInput = this.createNewExplicitEmbeddableInput( - embeddableId, - factory, - partial - ); - - return { - newPanel: { - type: factory.type, - explicitInput: { - ...explicitInput, - id: embeddableId, - version: factory.latestVersion, - } as TEmbeddableInput, - }, - otherPanels: this.getInput().panels, - }; - } - - protected getPanelState( - embeddableId: string - ) { - if (this.input.panels[embeddableId] === undefined) { - throw new PanelNotFoundError(); - } - const panelState: PanelState = this.input.panels[embeddableId]; - return panelState as PanelState; - } - - /** - * Return state that comes from the container and is passed down to the child. For instance, time range and - * filters are common inherited input state. Note that state stored in `this.input.panels[embeddableId].explicitInput` - * will override inherited input. - */ - protected abstract getInheritedInput(id: string): TChildInput; - - private async initializeChildEmbeddables( - initialInput: TContainerInput, - initializeSettings?: EmbeddableContainerSettings - ) { - let initializeOrder = Object.keys(initialInput.panels); - if (initializeSettings?.childIdInitializeOrder) { - const initializeOrderSet = new Set(); - - for (const id of [...initializeSettings.childIdInitializeOrder, ...initializeOrder]) { - if (!initializeOrderSet.has(id) && Boolean(this.getInput().panels[id])) { - initializeOrderSet.add(id); - } - } - - initializeOrder = Array.from(initializeOrderSet); - } - - for (const id of initializeOrder) { - if (initializeSettings?.initializeSequentially) { - const embeddable = await this.onPanelAdded(initialInput.panels[id]); - - if (embeddable && !isErrorEmbeddable(embeddable)) { - await this.untilEmbeddableLoaded(id); - } - } else { - this.onPanelAdded(initialInput.panels[id]); - } - } - } - - protected async createAndSaveEmbeddable< - TEmbeddableInput extends EmbeddableInput = EmbeddableInput, - TEmbeddable extends IEmbeddable = IEmbeddable - >(type: string, panelState: PanelState, otherPanels: TContainerInput['panels']) { - this.updateInput({ - panels: { - ...otherPanels, - [panelState.explicitInput.id]: panelState, - }, - } as Partial); - - return await this.untilEmbeddableLoaded(panelState.explicitInput.id); - } - - private createNewExplicitEmbeddableInput< - TEmbeddableInput extends EmbeddableInput = EmbeddableInput, - TEmbeddable extends IEmbeddable< - TEmbeddableInput, - EmbeddableOutput - > = IEmbeddable - >( - id: string, - factory: EmbeddableFactory, - partial: Partial = {} - ): Partial { - const inheritedInput = this.getInheritedInput(id); - const defaults = factory.getDefaultInput(partial); - - // Container input overrides defaults. - const explicitInput: Partial = partial; - - getKeys(defaults).forEach((key) => { - // @ts-ignore We know this key might not exist on inheritedInput. - const inheritedValue = inheritedInput[key]; - if (inheritedValue === undefined && explicitInput[key] === undefined) { - explicitInput[key] = defaults[key]; - } - }); - return explicitInput; - } - - private onPanelRemoved(id: string) { - // Clean up - const embeddable = this.getChild(id); - if (embeddable && embeddable.destroy) { - embeddable.destroy(); - - // Remove references. - const nextChildren = this.children$.value; - delete nextChildren[id]; - this.children$.next(nextChildren); - } - - this.updateOutput({ - embeddableLoaded: { - ...this.output.embeddableLoaded, - [id]: undefined, - }, - } as Partial); - } - - private async onPanelAdded(panel: PanelState) { - this.updateOutput({ - embeddableLoaded: { - ...this.output.embeddableLoaded, - [panel.explicitInput.id]: false, - }, - } as Partial); - let embeddable: IEmbeddable | ErrorEmbeddable | undefined; - const inputForChild = this.getInputForChild(panel.explicitInput.id); - try { - const factory = this.getFactory(panel.type); - if (!factory) { - throw new EmbeddableFactoryNotFoundError(panel.type); - } - - // TODO: lets get rid of this distinction with factories, I don't think it will be needed after this change. - embeddable = isSavedObjectEmbeddableInput(inputForChild) - ? await factory.createFromSavedObject(inputForChild.savedObjectId, inputForChild, this) - : await factory.create(inputForChild, this); - } catch (e) { - embeddable = new ErrorEmbeddable(e, { id: panel.explicitInput.id }, this); - } - - // EmbeddableFactory.create can return undefined without throwing an error, which indicates that an embeddable - // can't be created. This logic essentially only exists to support the current use case of - // visualizations being created from the add panel, which redirects the user to the visualize app. Once we - // switch over to inline creation we can probably clean this up, and force EmbeddableFactory.create to always - // return an embeddable, or throw an error. - if (embeddable) { - if (!embeddable.deferEmbeddableLoad) { - this.setChildLoaded(embeddable); - } - } else if (embeddable === undefined) { - this.removeEmbeddable(panel.explicitInput.id); - } - - return embeddable; - } - - private panelHasChanged(currentPanel: PanelState, prevPanel: PanelState) { - if (currentPanel.type !== prevPanel.type) { - return true; - } - } - - private maybeUpdateChildren( - currentPanels: TContainerInput['panels'], - prevPanels: TContainerInput['panels'] - ) { - const allIds = Object.keys({ ...currentPanels, ...this.output.embeddableLoaded }); - allIds.forEach((id) => { - if (currentPanels[id] !== undefined && this.output.embeddableLoaded[id] === undefined) { - return this.onPanelAdded(currentPanels[id]); - } - if (currentPanels[id] === undefined && this.output.embeddableLoaded[id] !== undefined) { - return this.onPanelRemoved(id); - } - // In case of type change, remove and add a panel with the same id - if (currentPanels[id] && prevPanels[id]) { - if (this.panelHasChanged(currentPanels[id], prevPanels[id])) { - this.onPanelRemoved(id); - this.onPanelAdded(currentPanels[id]); - } - } - }); - } -} diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts deleted file mode 100644 index 9ba8c41e50509..0000000000000 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - Embeddable, - EmbeddableInput, - EmbeddableOutput, - ErrorEmbeddable, - IEmbeddable, -} from '../embeddables'; -import { PanelState } from '../../../common/types'; - -export type { PanelState }; - -export interface ContainerOutput extends EmbeddableOutput { - embeddableLoaded: { [key: string]: boolean }; -} - -export interface ContainerInput extends EmbeddableInput { - hidePanelTitles?: boolean; - panels: { - [key: string]: PanelState; - }; -} - -export interface EmbeddableContainerSettings { - /** - * If true, the container will wait for each embeddable to load after creation before loading the next embeddable. - */ - initializeSequentially?: boolean; - /** - * Initialise children in the order specified. If an ID does not match it will be skipped and if a child is not included it will be initialized in the default order after the list of provided IDs. - */ - childIdInitializeOrder?: string[]; - - untilContainerInitialized?: () => Promise; -} - -export interface IContainer< - Inherited extends {} = {}, - I extends ContainerInput = ContainerInput, - O extends ContainerOutput = ContainerOutput -> extends IEmbeddable { - /** - * Call if you want to wait until an embeddable with that id has finished loading. - */ - untilEmbeddableLoaded( - id: string - ): Promise; - - /** - * Returns the input for the given child. Uses a combination of explicit input - * for the child stored on the parent and derived/inherited input taken from the - * container itself. - * @param id - */ - getInputForChild(id: string): EEI; - - /** - * Changes the input for a given child. Note, this will override all inherited state taken from - * the container itself. - * @param id - * @param changes - */ - updateInputForChild(id: string, changes: Partial): void; - - /** - * Returns the child embeddable with the given id. - * @param id - */ - getChild = Embeddable>(id: string): E; - - /** - * Embeddables which have deferEmbeddableLoad set to true need to manually call setChildLoaded - * on their parent container to communicate when they have finished loading. - * @param embeddable - the embeddable to set - */ - setChildLoaded(embeddable: E): void; - - /** - * Removes the embeddable with the given id. - * @param embeddableId - */ - removeEmbeddable(embeddableId: string): void; - - /** - * Adds a new embeddable to the container. `explicitInput` may partially specify the required embeddable input, - * but the remainder must come from inherited container state. - */ - addNewEmbeddable< - EEI extends EmbeddableInput = EmbeddableInput, - EEO extends EmbeddableOutput = EmbeddableOutput, - E extends Embeddable = Embeddable - >( - type: string, - explicitInput: Partial, - attributes?: unknown - ): Promise; - - replaceEmbeddable< - EEI extends EmbeddableInput = EmbeddableInput, - EEO extends EmbeddableOutput = EmbeddableOutput, - E extends Embeddable = Embeddable - >( - id: string, - newExplicitInput: Partial, - newType?: string, - generateNewId?: boolean - ): Promise; -} diff --git a/src/plugins/embeddable/public/lib/containers/index.ts b/src/plugins/embeddable/public/lib/containers/index.ts deleted file mode 100644 index 2f8a5dcf5d82b..0000000000000 --- a/src/plugins/embeddable/public/lib/containers/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export type { - IContainer, - PanelState, - ContainerInput, - ContainerOutput, - EmbeddableContainerSettings, -} from './i_container'; -export { Container } from './container'; diff --git a/src/plugins/embeddable/public/lib/embeddables/compatibility/embeddable_compatibility_utils.ts b/src/plugins/embeddable/public/lib/embeddables/compatibility/embeddable_compatibility_utils.ts index 84b31d5556f42..6512d28d6bcfd 100644 --- a/src/plugins/embeddable/public/lib/embeddables/compatibility/embeddable_compatibility_utils.ts +++ b/src/plugins/embeddable/public/lib/embeddables/compatibility/embeddable_compatibility_utils.ts @@ -9,15 +9,8 @@ import { ViewMode } from '@kbn/presentation-publishing'; import deepEqual from 'fast-deep-equal'; -import { - BehaviorSubject, - distinctUntilChanged, - distinctUntilKeyChanged, - map, - Subscription, -} from 'rxjs'; +import { BehaviorSubject, distinctUntilKeyChanged, map, Subscription } from 'rxjs'; import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../..'; -import { Container } from '../../containers'; import { ViewMode as LegacyViewMode } from '../../types'; import { CommonLegacyEmbeddable } from './legacy_embeddable_to_api'; @@ -33,33 +26,16 @@ export const embeddableInputToSubject = < const subject = new BehaviorSubject( embeddable.getExplicitInput()?.[key] as ValueType ); - if (useExplicitInput && embeddable.parent) { - subscription.add( - embeddable.parent - .getInput$() - .pipe( - distinctUntilChanged((prev, current) => { - const previousValue = (prev.panels[embeddable.id]?.explicitInput as LegacyInput)[key]; - const currentValue = (current.panels[embeddable.id]?.explicitInput as LegacyInput)?.[ - key - ]; - return deepEqual(previousValue, currentValue); - }) - ) - .subscribe(() => subject.next(embeddable.getExplicitInput()?.[key] as ValueType)) - ); - } else { - subscription.add( - embeddable - .getInput$() - .pipe( - distinctUntilKeyChanged(key, (prev, current) => { - return deepEqual(prev, current); - }) - ) - .subscribe(() => subject.next(embeddable.getInput()?.[key] as ValueType)) - ); - } + subscription.add( + embeddable + .getInput$() + .pipe( + distinctUntilKeyChanged(key, (prev, current) => { + return deepEqual(prev, current); + }) + ) + .subscribe(() => subject.next(embeddable.getInput()?.[key] as ValueType)) + ); return subject; }; @@ -122,20 +98,3 @@ export const viewModeToSubject = ( ); return subject; }; - -/** - * Temporarily copying types from dashboard_container.ts because we cannot import it here. - */ -interface DashboardRequiredMethods { - getExpandedPanelId: () => string | undefined; - setExpandedPanelId: (expandedPanelId: string | undefined) => void; -} - -export const hasDashboardRequiredMethods = ( - container: unknown -): container is DashboardRequiredMethods & Container => { - return ( - typeof (container as DashboardRequiredMethods).getExpandedPanelId === 'function' && - typeof (container as DashboardRequiredMethods).setExpandedPanelId === 'function' - ); -}; diff --git a/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts b/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts index 63c1a6593478b..56fe9b82d1b2d 100644 --- a/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts +++ b/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts @@ -184,7 +184,6 @@ export const legacyEmbeddableToApi = ( ); const uuid = embeddable.id; - const parentApi = embeddable.parent; const disableTriggers = embeddable.getInput().disableTriggers; /** @@ -248,7 +247,6 @@ export const legacyEmbeddableToApi = ( return { api: { - parentApi: parentApi as LegacyEmbeddableAPI['parentApi'], uuid, disableTriggers: disableTriggers ?? false, viewMode, diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts index b634c8d507510..13baf96962a3a 100644 --- a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts +++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts @@ -9,7 +9,6 @@ import { SavedObjectAttributes } from '@kbn/core/public'; import type { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common'; -import { IContainer } from '..'; import { EmbeddableFactory } from './embeddable_factory'; import { EmbeddableStateWithType } from '../../../common/types'; import { EmbeddableFactoryDefinition } from './embeddable_factory_definition'; @@ -41,7 +40,7 @@ export const defaultEmbeddableFactoryProvider = < : () => Promise.resolve({}), createFromSavedObject: def.createFromSavedObject ? def.createFromSavedObject.bind(def) - : (savedObjectId: string, input: Partial, parent?: IContainer) => { + : (savedObjectId: string, input: Partial, parent?: unknown) => { throw new Error(`Creation from saved object not supported by type ${def.type}`); }, create: (...args) => { diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index 9fc3598bcd5ad..40fb391400a18 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -13,9 +13,7 @@ import * as Rx from 'rxjs'; import { merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, map, skip } from 'rxjs'; import { RenderCompleteDispatcher } from '@kbn/kibana-utils-plugin/public'; -import { EmbeddableAppContext } from '@kbn/presentation-publishing'; import { Adapters } from '../types'; -import { IContainer } from '../containers'; import { EmbeddableError, EmbeddableOutput, @@ -48,8 +46,6 @@ export abstract class Embeddable< public readonly runtimeId = Embeddable.runtimeId++; - public readonly parent?: IContainer; - public readonly isContainer: boolean = false; public readonly deferEmbeddableLoad: boolean = false; public catchError?(error: EmbeddableError, domNode: HTMLElement | Element): TNode | (() => void); @@ -69,13 +65,9 @@ export abstract class Embeddable< protected renderComplete = new RenderCompleteDispatcher(); - // Listener to parent changes, if this embeddable exists in a parent, in order - // to update input when the parent changes. - private parentSubscription?: Rx.Subscription; - protected destroyed: boolean = false; - constructor(input: TEmbeddableInput, output: TEmbeddableOutput, parent?: IContainer) { + constructor(input: TEmbeddableInput, output: TEmbeddableOutput) { this.id = input.id; this.output = { @@ -93,20 +85,10 @@ export abstract class Embeddable< viewMode: ViewMode.EDIT, ...input, }; - this.parent = parent; this.inputSubject.next(this.input); this.outputSubject.next(this.output); - if (parent) { - this.parentSubscription = Rx.merge(parent.getInput$(), parent.getOutput$()).subscribe(() => { - // Make sure this panel hasn't been removed immediately after it was added, but before it finished loading. - if (!parent.getInput().panels[this.id]) return; - - const newInput = parent.getInputForChild(this.id); - this.onResetInput(newInput); - }); - } this.getOutput$() .pipe( map(({ title }) => title || ''), @@ -122,7 +104,6 @@ export abstract class Embeddable< onEdit: this.onEdit, viewMode: this.viewMode, dataViews: this.dataViews, - parentApi: this.parentApi, panelTitle: this.panelTitle, query$: this.query$, dataLoading: this.dataLoading, @@ -166,7 +147,6 @@ export abstract class Embeddable< public disableTriggers: LegacyEmbeddableAPI['disableTriggers']; public onEdit: LegacyEmbeddableAPI['onEdit']; public viewMode: LegacyEmbeddableAPI['viewMode']; - public parentApi: LegacyEmbeddableAPI['parentApi']; public dataViews: LegacyEmbeddableAPI['dataViews']; public query$: LegacyEmbeddableAPI['query$']; public panelTitle: LegacyEmbeddableAPI['panelTitle']; @@ -200,27 +180,10 @@ export abstract class Embeddable< return this.getOutput().editUrl ?? undefined; } - public getAppContext(): EmbeddableAppContext | undefined { - return this.parent?.getAppContext(); - } - public reportsEmbeddableLoad() { return false; } - public refreshInputFromParent() { - if (!this.parent) return; - // Make sure this panel hasn't been removed immediately after it was added, but before it finished loading. - if (!this.parent.getInput().panels[this.id]) return; - - const newInput = this.parent.getInputForChild(this.id); - this.onResetInput(newInput); - } - - public getIsContainer(): this is IContainer { - return this.isContainer === true; - } - /** * Reload will be called when there is a request to refresh the data or view, even if the * input data did not change. @@ -274,12 +237,6 @@ export abstract class Embeddable< } public getExplicitInput() { - const root = this.getRoot(); - if (root?.getIsContainer?.()) { - return ( - (root.getInput().panels?.[this.id]?.explicitInput as TEmbeddableInput) ?? this.getInput() - ); - } return this.getInput(); } @@ -299,28 +256,11 @@ export abstract class Embeddable< return this.output.description ?? ''; } - /** - * Returns the top most parent embeddable, or itself if this embeddable - * is not within a parent. - */ - public getRoot(): IEmbeddable | IContainer { - let root: IEmbeddable | IContainer = this; - while (root.parent) { - root = root.parent; - } - return root; - } - public updateInput(changes: Partial): void { if (this.destroyed) { throw new Error('Embeddable has been destroyed'); } - if (this.parent) { - // Ensures state changes flow from container downward. - this.parent.updateInputForChild(this.id, changes); - } else { - this.onInputChanged(changes); - } + this.onInputChanged(changes); } public render(el: HTMLElement): TNode | void { @@ -352,9 +292,6 @@ export abstract class Embeddable< this.outputSubject.complete(); this.destroyAPI(); - if (this.parentSubscription) { - this.parentSubscription.unsubscribe(); - } return; } @@ -374,9 +311,6 @@ export abstract class Embeddable< */ protected setInitializationFinished() { if (!this.deferEmbeddableLoad) return; - if (this.deferEmbeddableLoad && this.parent?.isContainer) { - this.parent.setChildLoaded(this); - } this.initializationFinished.complete(); } @@ -399,11 +333,6 @@ export abstract class Embeddable< protected onFatalError(e: Error) { this.fatalError = e; this.outputSubject.error(e); - // if the container is waiting for this embeddable to complete loading, - // a fatal error counts as complete. - if (this.deferEmbeddableLoad && this.parent?.isContainer) { - this.parent.setChildLoaded(this); - } } private onResetInput(newInput: TEmbeddableInput) { diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index ec5a849cc2da8..e363af639e252 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -13,7 +13,6 @@ import type { FinderAttributes } from '@kbn/saved-objects-finder-plugin/common'; import { UiActionsPresentableGrouping } from '@kbn/ui-actions-plugin/public'; import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; import { ErrorEmbeddable } from './error_embeddable'; -import { IContainer } from '../containers/i_container'; import { PropertySpec } from '../types'; import { EmbeddableStateWithType } from '../../../common/types'; @@ -125,7 +124,7 @@ export interface EmbeddableFactory< */ getExplicitInput( initialInput?: Partial, - parent?: IContainer + parent?: unknown ): Promise | ExplicitInputWithAttributes>; /** @@ -138,7 +137,7 @@ export interface EmbeddableFactory< createFromSavedObject( savedObjectId: string, input: Partial, - parent?: IContainer + parent?: unknown ): Promise; /** @@ -147,7 +146,7 @@ export interface EmbeddableFactory< */ create( initialInput: TEmbeddableInput, - parent?: IContainer + parent?: unknown ): Promise; order?: number; diff --git a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx index 52b74215c50e8..ffb5c197a4bcc 100644 --- a/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/error_embeddable.tsx @@ -11,7 +11,6 @@ import React, { ReactNode } from 'react'; import { PresentationPanelError } from '@kbn/presentation-panel-plugin/public'; -import { IContainer } from '../containers'; import { Embeddable } from './embeddable'; import { EmbeddableInput, EmbeddableOutput } from './i_embeddable'; @@ -23,8 +22,8 @@ export class ErrorEmbeddable extends Embeddable & - HasParentApi & EmbeddableHasTimeRange & PublishesSavedObjectId & CanLockHoverActions; @@ -89,18 +84,6 @@ export interface IEmbeddable< O extends EmbeddableOutput = EmbeddableOutput, N = any > extends LegacyEmbeddableAPI { - /** - * Is this embeddable an instance of a Container class, can it contain - * nested embeddables? - **/ - readonly isContainer: boolean; - - /** - * If this embeddable is nested inside a container, this will contain - * a reference to its parent. - **/ - readonly parent?: IContainer; - /** * The type of embeddable, this is what will be used to take a serialized * embeddable and find the correct factory for which to create an instance of it. @@ -147,12 +130,6 @@ export interface IEmbeddable< */ reportsEmbeddableLoad(): boolean; - /** - * A functional representation of the isContainer variable, but helpful for typescript to - * know the shape if this returns true - */ - getIsContainer(): this is IContainer; - /** * Get the input used to instantiate this embeddable. The input is a serialized representation of * this embeddable instance and can be used to clone or re-instantiate it. Input state: @@ -222,17 +199,6 @@ export interface IEmbeddable< */ getDescription(): string | undefined; - /** - * Returns the top most parent embeddable, or itself if this embeddable - * is not within a parent. - */ - getRoot(): IEmbeddable | IContainer; - - /** - * Returns the context of this embeddable's container, or undefined. - */ - getAppContext(): EmbeddableAppContext | undefined; - /** * Renders the embeddable at the given node. * @param domNode @@ -276,7 +242,5 @@ export interface IEmbeddable< */ getExplicitInputIsEqual(lastInput: Partial): Promise; - refreshInputFromParent(): void; - untilInitializationFinished(): Promise; } diff --git a/src/plugins/embeddable/public/lib/index.ts b/src/plugins/embeddable/public/lib/index.ts index bcae2e69ec407..511cc619bd5af 100644 --- a/src/plugins/embeddable/public/lib/index.ts +++ b/src/plugins/embeddable/public/lib/index.ts @@ -11,7 +11,6 @@ export * from './errors'; export * from './embeddables'; export * from './types'; export * from './triggers'; -export * from './containers'; export * from './state_transfer'; export * from './reference_or_value_embeddable'; export * from './self_styled_embeddable'; diff --git a/src/plugins/embeddable/public/store/create_store.test.ts b/src/plugins/embeddable/public/store/create_store.test.ts deleted file mode 100644 index 09413141d71c5..0000000000000 --- a/src/plugins/embeddable/public/store/create_store.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -// eslint-disable-next-line max-classes-per-file -import { createAction, createReducer, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import type { Store } from 'redux'; -import { - defaultEmbeddableFactoryProvider, - Container, - ContainerInput, - Embeddable, - EmbeddableInput, - EmbeddableOutput, -} from '../lib'; -import { createStore, State } from './create_store'; -import { input } from './input_slice'; -import { output } from './output_slice'; - -interface TestEmbeddableInput extends EmbeddableInput { - custom?: string; -} - -interface TestEmbeddableOutput extends EmbeddableOutput { - custom?: string; -} - -interface TestContainerInput extends ContainerInput { - custom?: string; -} - -class TestEmbeddable extends Embeddable { - type = 'test'; - reload = jest.fn(); - render = jest.fn(); -} - -class TestContainer extends Container, TestContainerInput> { - type = 'test'; - - getInheritedInput() { - return { - custom: this.input.custom, - }; - } -} - -describe('createStore', () => { - let embeddable: TestEmbeddable; - let store: Store>; - - beforeEach(() => { - embeddable = new TestEmbeddable({ id: '12345' }, { title: 'Test' }); - store = createStore(embeddable); - }); - - it('should populate the state with the embeddable input', () => { - expect(store.getState()).toHaveProperty('input', expect.objectContaining({ id: '12345' })); - }); - - it('should populate the state with the embeddable output', () => { - expect(store.getState()).toHaveProperty('output', expect.objectContaining({ title: 'Test' })); - }); - - it('should update the embeddable input on action dispatch', () => { - store.dispatch(input.actions.setTitle('Something')); - - expect(store.getState()).toHaveProperty('input.title', 'Something'); - }); - - it('should update the embeddable output on action dispatch', () => { - store.dispatch(output.actions.setTitle('Something')); - - expect(store.getState()).toHaveProperty('output.title', 'Something'); - }); - - it('should group input updates on multiple dispatch calls', async () => { - jest.spyOn(embeddable, 'updateInput'); - store.dispatch(input.actions.setTitle('Something')); - store.dispatch(input.actions.setHidePanelTitles(true)); - await new Promise((resolve) => setTimeout(resolve)); - - expect(embeddable.updateInput).toHaveBeenCalledTimes(1); - expect(embeddable.updateInput).nthCalledWith( - 1, - expect.objectContaining({ title: 'Something', hidePanelTitles: true }) - ); - }); - - it('should group output updates on multiple dispatch calls', async () => { - jest.spyOn(embeddable, 'updateOutput'); - store.dispatch(output.actions.setTitle('Something')); - store.dispatch(output.actions.setLoading(true)); - await new Promise((resolve) => setTimeout(resolve)); - - expect(embeddable.updateOutput).toHaveBeenCalledTimes(1); - expect(embeddable.updateOutput).nthCalledWith( - 1, - expect.objectContaining({ title: 'Something', loading: true }) - ); - }); - - it('should not update input on output changes', async () => { - jest.spyOn(embeddable, 'updateInput'); - store.dispatch(output.actions.setTitle('Something')); - await new Promise((resolve) => setTimeout(resolve)); - - expect(embeddable.updateInput).not.toHaveBeenCalled(); - }); - - it('should sync input changes', () => { - jest.spyOn(embeddable, 'updateInput'); - embeddable.updateInput({ title: 'Something' }); - - expect(embeddable.updateInput).toHaveBeenCalledTimes(1); - expect(store.getState()).toHaveProperty('input.title', 'Something'); - }); - - it('should sync output changes', () => { - jest.spyOn(embeddable, 'updateOutput'); - embeddable.updateOutput({ title: 'Something' }); - - expect(embeddable.updateOutput).toHaveBeenCalledTimes(1); - expect(store.getState()).toHaveProperty('output.title', 'Something'); - }); - - it('should provide a way to use a custom reducer', async () => { - const setCustom = createAction('custom'); - const customStore = createStore(embeddable, { - reducer: { - input: createReducer({} as TestEmbeddableInput, (builder) => - builder.addCase(setCustom, (state, action) => ({ ...state, custom: action.payload })) - ), - }, - }); - - jest.spyOn(embeddable, 'updateInput'); - customStore.dispatch(input.actions.setTitle('Something')); - customStore.dispatch(setCustom('Something else')); - await new Promise((resolve) => setTimeout(resolve)); - - expect(embeddable.updateInput).toHaveBeenCalledWith( - expect.objectContaining({ custom: 'Something else', title: 'Something' }) - ); - }); - - it('should provide a way to use a custom slice', async () => { - const slice = createSlice({ - name: 'test', - initialState: {} as State, - reducers: { - setCustom(state, action: PayloadAction) { - state.input.custom = action.payload; - state.output.custom = action.payload; - }, - }, - }); - const customStore = createStore(embeddable, { reducer: slice.reducer }); - - jest.spyOn(embeddable, 'updateInput'); - jest.spyOn(embeddable, 'updateOutput'); - customStore.dispatch(input.actions.setTitle('Something')); - customStore.dispatch(slice.actions.setCustom('Something else')); - await new Promise((resolve) => setTimeout(resolve)); - - expect(embeddable.updateInput).toHaveBeenCalledWith( - expect.objectContaining({ custom: 'Something else', title: 'Something' }) - ); - expect(embeddable.updateOutput).toHaveBeenCalledWith( - expect.objectContaining({ custom: 'Something else' }) - ); - }); - - describe('of a nested embeddable', () => { - const factory = defaultEmbeddableFactoryProvider< - TestEmbeddableInput, - TestEmbeddableOutput, - TestEmbeddable - >({ - type: 'test', - getDisplayName: () => 'Test', - isEditable: async () => true, - create: async (data, parent) => new TestEmbeddable(data, {}, parent), - }); - const getFactory = jest.fn().mockReturnValue(factory); - - let container: TestContainer; - - beforeEach(async () => { - container = new TestContainer( - { custom: 'something', id: 'id', panels: {} }, - { embeddableLoaded: {} }, - getFactory - ); - embeddable = (await container.addNewEmbeddable('test', { id: '12345' })) as TestEmbeddable; - store = createStore(embeddable); - }); - - it('should populate inherited input', () => { - expect(store.getState()).toHaveProperty('input.custom', 'something'); - }); - - it('should override inherited input on dispatch', async () => { - store.dispatch( - input.actions.update({ custom: 'something else' } as Partial) - ); - await new Promise((resolve) => setTimeout(resolve)); - - expect(store.getState()).toHaveProperty('input.custom', 'something else'); - expect(container.getInput()).not.toHaveProperty('input.custom'); - }); - - it('should restore value from the inherited input', async () => { - store.dispatch( - input.actions.update({ custom: 'something else' } as Partial) - ); - await new Promise((resolve) => setTimeout(resolve)); - store.dispatch(input.actions.update({ custom: undefined } as Partial)); - await new Promise((resolve) => setTimeout(resolve)); - - expect(store.getState()).toHaveProperty('input.custom', 'something'); - }); - - it('should not override inherited input on dispatch', async () => { - store.dispatch(input.actions.setTitle('Something')); - await new Promise((resolve) => setTimeout(resolve)); - container.updateInput({ custom: 'something else' }); - - expect(store.getState()).toHaveProperty( - 'input', - expect.objectContaining({ - title: 'Something', - custom: 'something else', - }) - ); - }); - }); -}); diff --git a/src/plugins/embeddable/public/store/create_store.ts b/src/plugins/embeddable/public/store/create_store.ts deleted file mode 100644 index a7b465a3c6db9..0000000000000 --- a/src/plugins/embeddable/public/store/create_store.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { chain, isEmpty, keys } from 'lodash'; -import { combineReducers, Reducer, Store, ReducersMapObject } from 'redux'; -import { configureStore, ConfigureStoreOptions } from '@reduxjs/toolkit'; -import { - debounceTime, - distinctUntilChanged, - filter, - last, - map, - pluck, - share, - takeUntil, - Observable, -} from 'rxjs'; -import reduceReducers from 'reduce-reducers'; -import type { Optional } from 'utility-types'; -import type { IEmbeddable } from '../lib'; -import { input } from './input_slice'; -import { output } from './output_slice'; - -export interface State { - input: E extends IEmbeddable ? I : never; - output: E extends IEmbeddable ? O : never; -} - -export interface CreateStoreOptions - extends Omit, 'reducer'> { - reducer?: Reducer | Optional, keyof State>; -} - -function createReducer( - reducer?: CreateStoreOptions['reducer'] -): Reducer | ReducersMapObject { - if (reducer instanceof Function) { - const generic = combineReducers>({ - input: input.reducer, - output: output.reducer, - }) as Reducer; - - return reduceReducers(generic, reducer) as Reducer; - } - - return { - ...(reducer ?? {}), - input: reducer?.input ? reduceReducers(input.reducer, reducer.input) : input.reducer, - output: reducer?.output ? reduceReducers(output.reducer, reducer.output) : output.reducer, - } as ReducersMapObject; -} - -function diff>(previous: T, current: T) { - return chain(current) - .keys() - .concat(keys(previous)) - .uniq() - .filter((key) => previous[key] !== current[key]) - .map((key) => [key, current[key]]) - .fromPairs() - .value() as Partial; -} - -/** - * Creates a Redux store for the given embeddable. - * @param embeddable The embeddable instance. - * @param options The custom options to pass to the `configureStore` call. - * @returns The Redux store. - */ -export function createStore = State>( - embeddable: E, - { preloadedState, reducer, ...options }: CreateStoreOptions = {} -): Store { - const store = configureStore({ - ...options, - preloadedState: { - input: embeddable.getInput(), - output: embeddable.getOutput(), - ...(preloadedState ?? {}), - } as NonNullable, - reducer: createReducer(reducer), - }); - - const state$ = new Observable((subscriber) => { - subscriber.add(store.subscribe(() => subscriber.next(store.getState()))); - }).pipe(share()); - const input$ = embeddable.getInput$(); - const output$ = embeddable.getOutput$(); - - state$ - .pipe( - takeUntil(input$.pipe(last())), - pluck('input'), - distinctUntilChanged(), - map((value) => diff(embeddable.getInput(), value)), - filter((patch) => !isEmpty(patch)), - debounceTime(0) - ) - .subscribe((patch) => embeddable.updateInput(patch)); - - state$ - .pipe( - takeUntil(output$.pipe(last())), - pluck('output'), - distinctUntilChanged(), - map((value) => diff(embeddable.getOutput(), value)), - filter((patch) => !isEmpty(patch)), - debounceTime(0) - ) - .subscribe((patch) => embeddable.updateOutput(patch)); - - input$ - .pipe( - map((value) => diff(store.getState().input, value)), - filter((patch) => !isEmpty(patch)) - ) - .subscribe((patch) => store.dispatch(input.actions.update(patch))); - - output$ - .pipe( - map((value) => diff(store.getState().output, value)), - filter((patch) => !isEmpty(patch)) - ) - .subscribe((patch) => store.dispatch(output.actions.update(patch))); - - return store; -} diff --git a/src/plugins/embeddable/public/store/index.ts b/src/plugins/embeddable/public/store/index.ts deleted file mode 100644 index eb75831b90bef..0000000000000 --- a/src/plugins/embeddable/public/store/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { input } from './input_slice'; -import { output } from './output_slice'; - -export type { CreateStoreOptions, State } from './create_store'; -export { createStore } from './create_store'; -export const actions = { - input: input.actions, - output: output.actions, -}; diff --git a/src/plugins/embeddable/public/store/input_slice.ts b/src/plugins/embeddable/public/store/input_slice.ts deleted file mode 100644 index 3b3cf8b8ee588..0000000000000 --- a/src/plugins/embeddable/public/store/input_slice.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import type { EmbeddableInput } from '../lib'; - -export const input = createSlice({ - name: 'input', - initialState: {} as EmbeddableInput, - reducers: { - setDisabledActions(state, action: PayloadAction) { - state.disabledActions = action.payload; - }, - setDisableTriggers(state, action: PayloadAction) { - state.disableTriggers = action.payload; - }, - setEnhancements(state, action: PayloadAction) { - state.enhancements = action.payload; - }, - setExecutionContext(state, action: PayloadAction) { - state.executionContext = action.payload; - }, - setHidePanelTitles(state, action: PayloadAction) { - state.hidePanelTitles = action.payload; - }, - setLastReloadRequestTime( - state, - action: PayloadAction - ) { - state.lastReloadRequestTime = action.payload; - }, - setSearchSessionId(state, action: PayloadAction) { - state.searchSessionId = action.payload; - }, - setSyncColors(state, action: PayloadAction) { - state.syncColors = action.payload; - }, - setSyncCursor(state, action: PayloadAction) { - state.syncCursor = action.payload; - }, - setSyncTooltips(state, action: PayloadAction) { - state.syncTooltips = action.payload; - }, - setTitle(state, action: PayloadAction) { - state.title = action.payload; - }, - setViewMode(state, action: PayloadAction) { - state.viewMode = action.payload; - }, - update(state, action: PayloadAction>) { - return { ...state, ...action.payload }; - }, - }, -}); diff --git a/src/plugins/embeddable/public/store/output_slice.ts b/src/plugins/embeddable/public/store/output_slice.ts deleted file mode 100644 index df53053d0e4a0..0000000000000 --- a/src/plugins/embeddable/public/store/output_slice.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import type { EmbeddableOutput } from '../lib'; - -export const output = createSlice({ - name: 'output', - initialState: {} as EmbeddableOutput, - reducers: { - setLoading(state, action: PayloadAction) { - state.loading = action.payload; - }, - setRendered(state, action: PayloadAction) { - state.rendered = action.payload; - }, - setError(state, action: PayloadAction) { - state.error = action.payload; - }, - setEditUrl(state, action: PayloadAction) { - state.editUrl = action.payload; - }, - setEditApp(state, action: PayloadAction) { - state.editApp = action.payload; - }, - setEditPath(state, action: PayloadAction) { - state.editPath = action.payload; - }, - setDefaultTitle(state, action: PayloadAction) { - state.defaultTitle = action.payload; - }, - setTitle(state, action: PayloadAction) { - state.title = action.payload; - }, - setEditable(state, action: PayloadAction) { - state.editable = action.payload; - }, - setSavedObjectId(state, action: PayloadAction) { - state.savedObjectId = action.payload; - }, - update(state, action: PayloadAction>) { - return { ...state, ...action.payload }; - }, - }, -}); diff --git a/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable.tsx b/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable.tsx deleted file mode 100644 index b70e1732f1e65..0000000000000 --- a/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { Embeddable, EmbeddableInput, IContainer } from '../..'; - -export const HELLO_WORLD_EMBEDDABLE = 'HELLO_WORLD_EMBEDDABLE'; - -export class HelloWorldEmbeddable extends Embeddable { - // The type of this embeddable. This will be used to find the appropriate factory - // to instantiate this kind of embeddable. - public readonly type = HELLO_WORLD_EMBEDDABLE; - - constructor(initialInput: EmbeddableInput, parent?: IContainer) { - super(initialInput, {}, parent); - } - - /** - * Render yourself at the dom node using whatever framework you like, angular, react, or just plain - * vanilla js. - * @param node - */ - public render(node: HTMLElement) { - node.innerHTML = '
HELLO WORLD!
'; - } - - /** - * This is mostly relevant for time based embeddables which need to update data - * even if EmbeddableInput has not changed at all. - */ - public reload() {} -} diff --git a/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable_factory.ts b/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable_factory.ts deleted file mode 100644 index 83c6d94748c56..0000000000000 --- a/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable_factory.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { i18n } from '@kbn/i18n'; -import { IContainer, EmbeddableInput, EmbeddableFactoryDefinition, EmbeddableFactory } from '../..'; -import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE } from './hello_world_embeddable'; - -export type HelloWorldEmbeddableFactory = EmbeddableFactory; -export class HelloWorldEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition { - public readonly type = HELLO_WORLD_EMBEDDABLE; - - /** - * In our simple example, we let everyone have permissions to edit this. Most - * embeddables should check the UI Capabilities service to be sure of - * the right permissions. - */ - public async isEditable() { - return true; - } - - public async create(initialInput: EmbeddableInput, parent?: IContainer) { - return new HelloWorldEmbeddable(initialInput, parent); - } - - public getDisplayName() { - return i18n.translate('embeddableApi.helloworld.displayName', { - defaultMessage: 'hello world', - }); - } -} diff --git a/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable_react.tsx b/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable_react.tsx deleted file mode 100644 index 42db1d55c4379..0000000000000 --- a/src/plugins/embeddable/public/tests/fixtures/hello_world_embeddable_react.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React from 'react'; -import { HelloWorldEmbeddable } from './hello_world_embeddable'; - -export class HelloWorldEmbeddableReact extends HelloWorldEmbeddable { - public render() { - return
HELLO WORLD!
; - } -} diff --git a/src/plugins/embeddable/public/tests/fixtures/index.ts b/src/plugins/embeddable/public/tests/fixtures/index.ts deleted file mode 100644 index 1f6f7a247ed86..0000000000000 --- a/src/plugins/embeddable/public/tests/fixtures/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export * from './hello_world_embeddable'; -export * from './hello_world_embeddable_factory'; -export * from './hello_world_embeddable_react'; diff --git a/src/plugins/image_embeddable/public/imports.ts b/src/plugins/image_embeddable/public/imports.ts index 1ec91560de8b4..dbe06b00daea9 100644 --- a/src/plugins/image_embeddable/public/imports.ts +++ b/src/plugins/image_embeddable/public/imports.ts @@ -18,11 +18,6 @@ export type { export type { FileImageMetadata } from '@kbn/shared-ux-file-types'; -export type { - IContainer, - EmbeddableInput, - EmbeddableFactoryDefinition, -} from '@kbn/embeddable-plugin/public'; export type { ApplicationStart, OverlayStart, ThemeServiceStart } from '@kbn/core/public'; export type { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public'; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 54b37b0a237e1..4c3dc07a9eb5d 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -10,7 +10,7 @@ import { PublicContract } from '@kbn/utility-types'; import { PluginInitializerContext } from '@kbn/core/public'; import { VisualizationsPlugin, VisualizationsSetup, VisualizationsStart } from './plugin'; -import type { VisualizeEmbeddableFactory, VisualizeEmbeddable } from './legacy/embeddable'; +import type { VisualizeEmbeddable } from './legacy/embeddable'; export function plugin(initializerContext: PluginInitializerContext) { return new VisualizationsPlugin(initializerContext); @@ -38,7 +38,6 @@ export type { } from './vis_types'; export type { VisualizeEditorInput } from './embeddable/types'; export type { Vis, SerializedVis, SerializedVisData, VisData } from './vis'; -export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; export type { SchemaConfig } from '../common/types'; export { updateOldState } from './legacy/vis_update_state'; diff --git a/src/plugins/visualizations/public/legacy/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/legacy/embeddable/create_vis_embeddable_from_object.ts index b684bd83402c5..76acb5da63c53 100644 --- a/src/plugins/visualizations/public/legacy/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/legacy/embeddable/create_vis_embeddable_from_object.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { IContainer, ErrorEmbeddable } from '@kbn/embeddable-plugin/public'; +import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import { Vis } from '../../types'; import type { @@ -16,10 +16,10 @@ import type { VisualizeByValueInput, VisualizeByReferenceInput, VisualizeSavedObjectAttributes, + VisualizeEmbeddableDeps, } from './visualize_embeddable'; import { getHttp, getTimeFilter, getCapabilities } from '../../services'; import { urlFor } from '../../utils/saved_visualize_utils'; -import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { createVisualizeEmbeddableAsync } from './visualize_embeddable_async'; import { AttributeService } from './attribute_service'; @@ -28,7 +28,7 @@ import { AttributeService } from './attribute_service'; * used within the visualize editor. */ export const createVisEmbeddableFromObject = - (deps: VisualizeEmbeddableFactoryDeps) => + (deps: VisualizeEmbeddableDeps) => async ( vis: Vis, input: Partial & { id: string }, @@ -36,8 +36,7 @@ export const createVisEmbeddableFromObject = VisualizeSavedObjectAttributes, VisualizeByValueInput, VisualizeByReferenceInput - >, - parent?: IContainer + > ): Promise => { try { const visId = vis.id as string; @@ -75,11 +74,10 @@ export const createVisEmbeddableFromObject = capabilities, }, input, - attributeService, - parent + attributeService ); } catch (e) { console.error(e); // eslint-disable-line no-console - return new ErrorEmbeddable(e, input, parent); + return new ErrorEmbeddable(e, input); } }; diff --git a/src/plugins/visualizations/public/legacy/embeddable/index.ts b/src/plugins/visualizations/public/legacy/embeddable/index.ts index 979a631f8c665..6aa08f7b847de 100644 --- a/src/plugins/visualizations/public/legacy/embeddable/index.ts +++ b/src/plugins/visualizations/public/legacy/embeddable/index.ts @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { VisualizeEmbeddableFactory } from './visualize_embeddable_factory'; export { VISUALIZE_EMBEDDABLE_TYPE, COMMON_VISUALIZATION_GROUPING } from './constants'; export { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; diff --git a/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx index bfd87435345e5..3ef1947dc5d3d 100644 --- a/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable.tsx @@ -27,7 +27,6 @@ import { EmbeddableInput, EmbeddableOutput, FilterableEmbeddable, - IContainer, ReferenceOrValueEmbeddable, SavedObjectEmbeddableInput, } from '@kbn/embeddable-plugin/public'; @@ -41,18 +40,28 @@ import type { RenderMode } from '@kbn/expressions-plugin/common'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/public'; import { mapAndFlattenFilters } from '@kbn/data-plugin/public'; import { isChartSizeEvent } from '@kbn/chart-expressions-common'; +import { StartServicesGetter } from '@kbn/kibana-utils-plugin/public'; import { isFallbackDataView } from '../../visualize_app/utils'; import { VisualizationMissedSavedObjectError } from '../../components/visualization_missed_saved_object_error'; import VisualizationError from '../../components/visualization_error'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { SerializedVis, Vis } from '../../vis'; -import { getApplication, getExecutionContext, getExpressions, getUiActions } from '../../services'; +import { getApplication, getExpressions, getUiActions } from '../../services'; import { VIS_EVENT_TO_TRIGGER } from '../../embeddable/events'; -import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { getSavedVisualization } from '../../utils/saved_visualize_utils'; import { VisSavedObject } from '../../types'; import { toExpressionAst } from '../../embeddable/to_ast'; import { AttributeService } from './attribute_service'; +import { VisualizationsStartDeps } from '../../plugin'; + +export interface VisualizeEmbeddableDeps { + start: StartServicesGetter< + Pick< + VisualizationsStartDeps, + 'inspector' | 'embeddable' | 'data' | 'savedObjectsTaggingOss' | 'spaces' + > + >; +} export interface VisualizeEmbeddableConfiguration { vis: Vis; @@ -60,7 +69,7 @@ export interface VisualizeEmbeddableConfiguration { editPath: string; editUrl: string; capabilities: { visualizeSave: boolean; dashboardSave: boolean; visualizeOpen: boolean }; - deps: VisualizeEmbeddableFactoryDeps; + deps: VisualizeEmbeddableDeps; } export interface VisualizeInput extends EmbeddableInput { @@ -120,7 +129,7 @@ export class VisualizeEmbeddable private warningDomNode: any; public readonly type = VISUALIZE_EMBEDDABLE_TYPE; private abortController?: AbortController; - private readonly deps: VisualizeEmbeddableFactoryDeps; + private readonly deps: VisualizeEmbeddableDeps; private readonly inspectorAdapters?: Adapters; private attributeService?: AttributeService< VisualizeSavedObjectAttributes, @@ -140,22 +149,17 @@ export class VisualizeEmbeddable VisualizeSavedObjectAttributes, VisualizeByValueInput, VisualizeByReferenceInput - >, - parent?: IContainer + > ) { - super( - initialInput, - { - defaultTitle: vis.title, - defaultDescription: vis.description, - editPath, - editApp: 'visualize', - editUrl, - indexPatterns, - visTypeName: vis.type.name, - }, - parent - ); + super(initialInput, { + defaultTitle: vis.title, + defaultDescription: vis.description, + editPath, + editApp: 'visualize', + editUrl, + indexPatterns, + visTypeName: vis.type.name, + }); this.deps = deps; this.timefilter = timefilter; this.syncColors = this.input.syncColors; @@ -270,8 +274,6 @@ export class VisualizeEmbeddable this.vis.uiState.on('change', this.uiStateChangeHandler); } - } else if (this.parent) { - this.vis.uiState.clearAllKeys(); } } @@ -572,7 +574,6 @@ export class VisualizeEmbeddable }; private getExecutionContext() { - const parentContext = this.parent?.getInput().executionContext || getExecutionContext().get(); const child: KibanaExecutionContext = { type: 'agg_based', name: this.vis.type.name, @@ -582,7 +583,6 @@ export class VisualizeEmbeddable }; return { - ...parentContext, child, }; } diff --git a/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.test.ts b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.test.ts deleted file mode 100644 index 61f5cee020503..0000000000000 --- a/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; -import { VisualizeEmbeddableFactory, VisualizeInput } from '.'; -import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; - -describe('visualize_embeddable_factory', () => { - const factory = new VisualizeEmbeddableFactory({} as VisualizeEmbeddableFactoryDeps); - test('extract saved search references for search source state and not store them in state', () => { - const { state, references } = factory.extract({ - savedVis: { - type: 'area', - params: {}, - uiState: {}, - data: { - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: {}, - schema: 'metric', - }, - ], - searchSource: { - query: { - query: '', - language: 'kuery', - }, - filter: [], - }, - savedSearchId: '123', - }, - }, - enhancements: {}, - type: 'visualization', - } as unknown as EmbeddableStateWithType); - expect(references).toEqual([ - { - type: 'search', - name: 'search_0', - id: '123', - }, - ]); - expect((state as unknown as VisualizeInput).savedVis?.data.savedSearchId).toBeUndefined(); - }); - - test('extract data view references for search source state and not store them in state', () => { - const { state, references } = factory.extract({ - savedVis: { - type: 'area', - params: {}, - uiState: {}, - data: { - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: {}, - schema: 'metric', - }, - ], - searchSource: { - query: { - query: '', - language: 'kuery', - }, - index: '123', - filter: [], - }, - }, - }, - enhancements: {}, - type: 'visualization', - } as unknown as EmbeddableStateWithType); - expect(references).toEqual([ - { - type: 'index-pattern', - name: ( - (state as unknown as VisualizeInput).savedVis?.data.searchSource as { - indexRefName: string; - } - ).indexRefName, - id: '123', - }, - ]); - expect((state as unknown as VisualizeInput).savedVis?.data.searchSource.index).toBeUndefined(); - }); - - test('inject data view references into search source state', () => { - const embeddedState = factory.inject( - { - savedVis: { - type: 'area', - params: {}, - uiState: {}, - data: { - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: {}, - schema: 'metric', - }, - ], - searchSource: { - query: { - query: '', - language: 'kuery', - }, - indexRefName: 'x', - filter: [], - }, - }, - }, - enhancements: {}, - type: 'visualization', - } as unknown as EmbeddableStateWithType, - [{ name: 'x', id: '123', type: 'index-pattern' }] - ) as VisualizeInput; - expect(embeddedState.savedVis!.data.searchSource.index).toBe('123'); - expect( - (embeddedState.savedVis!.data.searchSource as { indexRefName: string }).indexRefName - ).toBe(undefined); - }); - - test('inject data view reference into search source state even if it is in injected state already', () => { - const embeddedState = factory.inject( - { - savedVis: { - type: 'area', - params: {}, - uiState: {}, - data: { - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: {}, - schema: 'metric', - }, - ], - searchSource: { - query: { - query: '', - language: 'kuery', - }, - index: '456', - filter: [], - }, - }, - }, - enhancements: {}, - type: 'visualization', - } as unknown as EmbeddableStateWithType, - [{ name: 'kibanaSavedObjectMeta.searchSourceJSON.index', id: '123', type: 'index-pattern' }] - ) as VisualizeInput; - expect(embeddedState.savedVis!.data.searchSource.index).toBe('123'); - expect( - (embeddedState.savedVis!.data.searchSource as { indexRefName: string }).indexRefName - ).toBe(undefined); - }); - - test('inject search reference into search source state', () => { - const embeddedState = factory.inject( - { - savedVis: { - type: 'area', - params: {}, - uiState: {}, - data: { - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: {}, - schema: 'metric', - }, - ], - searchSource: { - query: { - query: '', - language: 'kuery', - }, - filter: [], - }, - }, - }, - enhancements: {}, - type: 'visualization', - } as unknown as EmbeddableStateWithType, - [{ name: 'search_0', id: '123', type: 'search' }] - ); - expect((embeddedState as VisualizeInput).savedVis!.data.savedSearchId).toBe('123'); - }); - - test('inject search reference into search source state even if it is injected already', () => { - const embeddedState = factory.inject( - { - savedVis: { - type: 'area', - params: {}, - uiState: {}, - data: { - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - params: {}, - schema: 'metric', - }, - ], - searchSource: { - query: { - query: '', - language: 'kuery', - }, - filter: [], - }, - savedSearchId: '789', - }, - }, - enhancements: {}, - type: 'visualization', - } as unknown as EmbeddableStateWithType, - [{ name: 'search_0', id: '123', type: 'search' }] - ); - expect((embeddedState as VisualizeInput).savedVis!.data.savedSearchId).toBe('123'); - }); -}); diff --git a/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.tsx deleted file mode 100644 index 112a8d3b7fd8c..0000000000000 --- a/src/plugins/visualizations/public/legacy/embeddable/visualize_embeddable_factory.tsx +++ /dev/null @@ -1,357 +0,0 @@ -/* - * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { i18n } from '@kbn/i18n'; -import { first } from 'rxjs'; -import type { OnSaveProps } from '@kbn/saved-objects-plugin/public'; -import type { SavedObjectMetaData } from '@kbn/saved-objects-finder-plugin/public'; -import type { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; - -import { - injectSearchSourceReferences, - extractSearchSourceReferences, - SerializedSearchSourceFields, -} from '@kbn/data-plugin/public'; -import type { SavedObjectAttributes, SavedObjectReference } from '@kbn/core/public'; - -import { - EmbeddableFactoryDefinition, - EmbeddableOutput, - ErrorEmbeddable, - IContainer, -} from '@kbn/embeddable-plugin/public'; -import type { StartServicesGetter } from '@kbn/kibana-utils-plugin/public'; -import { AttributeService } from './attribute_service'; -import { checkForDuplicateTitle } from '../../utils/saved_objects_utils/check_for_duplicate_title'; -import type { - VisualizeByReferenceInput, - VisualizeByValueInput, - VisualizeEmbeddable, - VisualizeInput, - VisualizeOutput, - VisualizeSavedObjectAttributes, -} from './visualize_embeddable'; -import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; -import type { SerializedVis, Vis } from '../../vis'; -import { createVisAsync } from '../../vis_async'; -import { getCapabilities, getTypes } from '../../services'; -import { showNewVisModal } from '../../wizard'; -import { - convertToSerializedVis, - getSavedVisualization, - saveVisualization, - getFullPath, -} from '../../utils/saved_visualize_utils'; -import { - extractControlsReferences, - extractTimeSeriesReferences, - injectTimeSeriesReferences, - injectControlsReferences, -} from '../../utils/saved_visualization_references'; -import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; -import type { VisualizationsStartDeps } from '../../plugin'; - -interface VisualizationAttributes extends SavedObjectAttributes { - title: string; - visState: string; -} - -export interface VisualizeEmbeddableFactoryDeps { - start: StartServicesGetter< - Pick< - VisualizationsStartDeps, - 'inspector' | 'embeddable' | 'data' | 'savedObjectsTaggingOss' | 'spaces' - > - >; -} - -/** @deprecated - * VisualizeEmbeddable is no longer registered with the legacy embeddable system and is only - * used within the visualize editor. - */ -export class VisualizeEmbeddableFactory - implements - EmbeddableFactoryDefinition< - VisualizeInput, - VisualizeOutput | EmbeddableOutput, - VisualizeEmbeddable, - VisualizationAttributes - > -{ - public readonly type = VISUALIZE_EMBEDDABLE_TYPE; - - private attributeService?: AttributeService< - VisualizeSavedObjectAttributes, - VisualizeByValueInput, - VisualizeByReferenceInput - >; - - public readonly savedObjectMetaData: SavedObjectMetaData = { - name: i18n.translate('visualizations.savedObjectName', { defaultMessage: 'Visualization' }), - includeFields: ['visState'], - type: 'visualization', - getIconForSavedObject: (savedObject) => { - return ( - getTypes().get(JSON.parse(savedObject.attributes.visState).type)?.icon || 'visualizeApp' - ); - }, - getTooltipForSavedObject: (savedObject) => { - return `${savedObject.attributes.title} (${ - getTypes().get(JSON.parse(savedObject.attributes.visState).type)?.title - })`; - }, - showSavedObject: (savedObject) => { - try { - const typeName: string = JSON.parse(savedObject.attributes.visState).type; - const visType = getTypes().get(typeName); - return Boolean(visType); - } catch { - return false; - } - }, - getSavedObjectSubType: (savedObject) => { - return JSON.parse(savedObject.attributes.visState).type; - }, - }; - - constructor(private readonly deps: VisualizeEmbeddableFactoryDeps) {} - - public async isEditable() { - return getCapabilities().visualize.save as boolean; - } - - public getDisplayName() { - return i18n.translate('visualizations.displayName', { - defaultMessage: 'visualization', - }); - } - - public async getCurrentAppId() { - return this.deps.start().core.application.currentAppId$.pipe(first()).toPromise(); - } - - private async getAttributeService() { - if (!this.attributeService) { - this.attributeService = new AttributeService(this.type, { - saveMethod: this.saveMethod.bind(this), - checkForDuplicateTitle: this.checkTitle.bind(this), - }); - } - return this.attributeService!; - } - - public async createFromSavedObject( - savedObjectId: string, - input: Partial & { id: string }, - parent?: IContainer - ): Promise { - const startDeps = this.deps.start(); - - try { - const savedObject = await getSavedVisualization( - { - search: startDeps.plugins.data.search, - dataViews: startDeps.plugins.data.dataViews, - spaces: startDeps.plugins.spaces, - savedObjectsTagging: startDeps.plugins.savedObjectsTaggingOss?.getTaggingApi(), - ...startDeps.core, - }, - savedObjectId - ); - - if (savedObject.sharingSavedObjectProps?.outcome === 'conflict') { - return new ErrorEmbeddable( - i18n.translate('visualizations.embeddable.legacyURLConflict.errorMessage', { - defaultMessage: `This visualization has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`, - values: { json: savedObject.sharingSavedObjectProps?.errorJSON }, - }), - input, - parent - ); - } - const visState = convertToSerializedVis(savedObject); - const vis = await createVisAsync(savedObject.visState.type, visState); - - return createVisEmbeddableFromObject(this.deps)( - vis, - input, - await this.getAttributeService(), - parent - ); - } catch (e) { - console.error(e); // eslint-disable-line no-console - return new ErrorEmbeddable(e, input, parent); - } - } - - public async create(input: VisualizeInput & { savedVis?: SerializedVis }, parent?: IContainer) { - // TODO: This is a bit of a hack to preserve the original functionality. Ideally we will clean this up - // to allow for in place creation of visualizations without having to navigate away to a new URL. - if (input.savedVis) { - const visState = input.savedVis; - const vis = await createVisAsync(visState.type, visState); - return createVisEmbeddableFromObject(this.deps)( - vis, - input, - await this.getAttributeService(), - parent - ); - } else { - showNewVisModal({ - originatingApp: await this.getCurrentAppId(), - outsideVisualizeApp: true, - }); - return undefined; - } - } - - private async saveMethod(attributes: VisualizeSavedObjectAttributes): Promise<{ id: string }> { - try { - const { title, savedVis } = attributes; - const visObj = attributes.vis; - if (!savedVis) { - throw new Error('No Saved Vis'); - } - const saveOptions = { - confirmOverwrite: false, - returnToOrigin: true, - isTitleDuplicateConfirmed: true, - copyOnSave: false, - }; - savedVis.title = title; - savedVis.description = ''; - savedVis.searchSourceFields = visObj?.data.searchSource?.getSerializedFields(); - savedVis.savedSearchId = visObj?.data.savedSearchId; - const serializedVis = (visObj as unknown as Vis).serialize(); - const { params, data } = serializedVis; - savedVis.visState = { - title, - type: serializedVis.type, - params, - aggs: data.aggs, - }; - if (visObj) { - savedVis.uiStateJSON = visObj?.uiState.toString(); - } - const { core, plugins } = this.deps.start(); - const id = await saveVisualization(savedVis, saveOptions, { - savedObjectsTagging: plugins.savedObjectsTaggingOss?.getTaggingApi(), - ...core, - }); - if (!id || id === '') { - throw new Error( - i18n.translate('visualizations.savingVisualizationFailed.errorMsg', { - defaultMessage: 'Saving a visualization failed', - }) - ); - } - core.chrome.recentlyAccessed.add(getFullPath(id), savedVis.title, String(id)); - return { id }; - } catch (error) { - throw error; - } - } - - public async checkTitle(props: OnSaveProps): Promise { - const { core } = this.deps.start(); - - return checkForDuplicateTitle( - { - title: props.newTitle, - lastSavedTitle: '', - getEsType: () => this.type, - }, - false, - props.isTitleDuplicateConfirmed, - props.onTitleDuplicate, - core - ); - } - - public inject(_state: EmbeddableStateWithType, references: SavedObjectReference[]) { - let state = _state as unknown as VisualizeInput; - - const { type, params } = state.savedVis ?? {}; - - if (type && params) { - injectControlsReferences(type, params, references); - injectTimeSeriesReferences(type, params, references); - } - - if (state.savedVis?.data.searchSource) { - let extractedSearchSource = state.savedVis?.data - .searchSource as SerializedSearchSourceFields & { - indexRefName: string; - }; - if (!('indexRefName' in state.savedVis.data.searchSource)) { - // due to a bug in 8.0, some visualizations were saved with an injected state - re-extract in that case and inject the upstream references because they might have changed - extractedSearchSource = extractSearchSourceReferences( - extractedSearchSource - )[0] as SerializedSearchSourceFields & { - indexRefName: string; - }; - } - const injectedSearchSource = injectSearchSourceReferences(extractedSearchSource, references); - state = { - ...state, - savedVis: { - ...state.savedVis, - data: { - ...state.savedVis.data, - searchSource: injectedSearchSource, - savedSearchId: references.find((r) => r.name === 'search_0')?.id, - }, - }, - }; - } - - return state as EmbeddableStateWithType; - } - - public extract(_state: EmbeddableStateWithType) { - let state = _state as unknown as VisualizeInput; - const references = []; - - if (state.savedVis?.data.savedSearchId) { - references.push({ - name: 'search_0', - type: 'search', - id: String(state.savedVis.data.savedSearchId), - }); - } - - if (state.savedVis?.data.searchSource) { - const [extractedSearchSource, searchSourceReferences] = extractSearchSourceReferences( - state.savedVis.data.searchSource - ); - - references.push(...searchSourceReferences); - state = { - ...state, - savedVis: { - ...state.savedVis, - data: { - ...state.savedVis.data, - searchSource: extractedSearchSource, - savedSearchId: undefined, - }, - }, - }; - } - - const { type, params } = state.savedVis ?? {}; - - if (type && params) { - extractControlsReferences(type, params, references, `control_${state.id}`); - extractTimeSeriesReferences(type, params, references, `metrics_${state.id}`); - } - - return { state: state as EmbeddableStateWithType, references }; - } -} diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 5b0dbcbc531bd..8bde925660931 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -2805,7 +2805,6 @@ "embeddableApi.errors.embeddableFactoryNotFound": "Impossible de charger {type}. Veuillez effectuer une mise à niveau vers la distribution par défaut d'Elasticsearch et de Kibana avec la licence appropriée.", "embeddableApi.errors.paneldoesNotExist": "Panneau introuvable", "embeddableApi.errors.panelIncompatibleError": "L'API du panneau n'est pas compatible", - "embeddableApi.helloworld.displayName": "bonjour", "embeddableApi.multiValueClickTrigger.description": "Sélection de plusieurs valeurs d'une même dimension dans la visualisation", "embeddableApi.multiValueClickTrigger.title": "Clics multiples", "embeddableApi.panelBadgeTrigger.description": "Des actions apparaissent dans la barre de titre lorsqu'un élément pouvant être intégré est chargé dans un panneau.", @@ -9620,7 +9619,6 @@ "visualizations.editVisualization.readOnlyErrorMessage": "Les visualisations {visTypeTitle} sont en lecture seule et ne peuvent pas être ouvertes dans l'éditeur", "visualizations.embeddable.errorTitle": "Impossible de charger la visualisation", "visualizations.embeddable.inspectorTitle": "Inspecteur", - "visualizations.embeddable.legacyURLConflict.errorMessage": "Cette visualisation a la même URL qu'un alias hérité. Désactiver l'alias pour résoudre cette erreur : {json}", "visualizations.embeddable.placeholderTitle": "Titre de l'espace réservé", "visualizations.embeddable.tsdbRollupWarning": "La visualisation utilise une fonction qui n'est pas prise en charge par les données cumulées. Sélectionnez une autre fonction ou modifiez la plage temporelle.", "visualizations.experimentalVisInfoText": "Elle pourra être modifiée ou supprimée totalement dans une prochaine version. Elastic s'efforcera de corriger tous les problèmes, mais les fonctionnalités en version d'évaluation technique ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale. Pour apporter des commentaires, veuillez créer une entrée dans {githubLink}.", @@ -9689,9 +9687,7 @@ "visualizations.pageHeading": "Visualisation {chartType} {chartName}", "visualizations.readOnlyLegacyVisMessage": "Ces détails ne peuvent pas être modifiés, car cette visualisation n'est plus prise en charge.", "visualizations.reporting.defaultReportTitle": "Visualisation [{date}]", - "visualizations.savedObjectName": "Visualisation", "visualizations.saveDuplicateRejectedDescription": "La confirmation d'enregistrement avec un doublon de titre a été rejetée.", - "visualizations.savingVisualizationFailed.errorMsg": "L'enregistrement de la visualisation a échoué", "visualizations.search.label": "Recherche", "visualizations.share.shareModal.title": "Partager la visualisation", "visualizations.tonNavMenu.tryItBadgeText": "Essayer", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 56ab7098b7701..b72b16da30b1c 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -2800,7 +2800,6 @@ "embeddableApi.errors.embeddableFactoryNotFound": "{type} を読み込めません。Elasticsearch と Kibanaのデフォルトのディストリビューションを適切なライセンスでアップグレードしてください。", "embeddableApi.errors.paneldoesNotExist": "パネルが見つかりません", "embeddableApi.errors.panelIncompatibleError": "パネルAPIに互換性がありません", - "embeddableApi.helloworld.displayName": "こんにちは", "embeddableApi.multiValueClickTrigger.description": "ビジュアライゼーションの1つのディメンションの複数値を選択しています", "embeddableApi.multiValueClickTrigger.title": "マルチクリック", "embeddableApi.panelBadgeTrigger.description": "パネルに埋め込み可能なファイルが読み込まれるときに、アクションがタイトルバーに表示されます。", @@ -9495,7 +9494,6 @@ "visualizations.editVisualization.readOnlyErrorMessage": "{visTypeTitle}ビジュアライゼーションは読み取り専用であり、エディターで開くことができません。", "visualizations.embeddable.errorTitle": "ビジュアライゼーションを読み込めません", "visualizations.embeddable.inspectorTitle": "インスペクター", - "visualizations.embeddable.legacyURLConflict.errorMessage": "このビジュアライゼーションにはレガシーエイリアスと同じURLがあります。このエラーを解決するには、エイリアスを無効にしてください:{json}", "visualizations.embeddable.placeholderTitle": "プレースホルダータイトル", "visualizations.embeddable.tsdbRollupWarning": "ビジュアライゼーションは、ロールアップされたデータによってサポートされていない関数を使用しています。別の関数を選択するか、時間範囲を選択してください。", "visualizations.experimentalVisInfoText": "将来のリリースでは、変更されるか、完全に削除される場合があります。Elasticはすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。フィードバックがある場合は、{githubLink}で問題を報告してください。", @@ -9564,9 +9562,7 @@ "visualizations.pageHeading": "{chartName} {chartType}ビジュアライゼーション", "visualizations.readOnlyLegacyVisMessage": "これらの詳細は編集できません。このビジュアライゼーションはサポートされていません。", "visualizations.reporting.defaultReportTitle": "ビジュアライゼーション[{date}]", - "visualizations.savedObjectName": "ビジュアライゼーション", "visualizations.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました", - "visualizations.savingVisualizationFailed.errorMsg": "ビジュアライゼーションの保存が失敗しました", "visualizations.search.label": "検索", "visualizations.share.shareModal.title": "このビジュアライゼーションを共有", "visualizations.tonNavMenu.tryItBadgeText": "お試しください", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index ca886fadab225..b7acfdb84cdc7 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -2790,7 +2790,6 @@ "embeddableApi.errors.embeddableFactoryNotFound": "{type} 无法加载。请升级到具有适当许可的默认 Elasticsearch 和 Kibana 分发。", "embeddableApi.errors.paneldoesNotExist": "未找到面板", "embeddableApi.errors.panelIncompatibleError": "面板 API 不兼容", - "embeddableApi.helloworld.displayName": "hello world", "embeddableApi.multiValueClickTrigger.description": "在可视化上选择多个单一维度的值", "embeddableApi.multiValueClickTrigger.title": "多次单击", "embeddableApi.panelBadgeTrigger.description": "可嵌入对象在面板加载后,操作便显示在标题栏中。", @@ -9347,7 +9346,6 @@ "visualizations.editVisualization.readOnlyErrorMessage": "{visTypeTitle} 可视化为只读状态,无法在编辑器中打开", "visualizations.embeddable.errorTitle": "无法加载可视化", "visualizations.embeddable.inspectorTitle": "检查器", - "visualizations.embeddable.legacyURLConflict.errorMessage": "此可视化具有与旧版别名相同的 URL。请禁用别名以解决此错误:{json}", "visualizations.embeddable.placeholderTitle": "占位符标题", "visualizations.embeddable.tsdbRollupWarning": "可视化使用的函数不受汇总/打包数据支持。请选择其他函数,或更改时间范围。", "visualizations.experimentalVisInfoText": "在未来版本中可能会更改或完全移除。Elastic 将努力修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。如欲提供反馈,请在 {githubLink} 中创建问题。", @@ -9415,9 +9413,7 @@ "visualizations.pageHeading": "{chartName} {chartType} 可视化", "visualizations.readOnlyLegacyVisMessage": "无法编辑这些详情,因为不再支持此可视化。", "visualizations.reporting.defaultReportTitle": "可视化 [{date}]", - "visualizations.savedObjectName": "可视化", "visualizations.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认", - "visualizations.savingVisualizationFailed.errorMsg": "保存可视化失败", "visualizations.search.label": "搜索", "visualizations.share.shareModal.title": "共享此可视化", "visualizations.tonNavMenu.tryItBadgeText": "试用", diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx index 3f5afc4065e1f..fe844ae26905b 100644 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx +++ b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable_factory.tsx @@ -32,7 +32,7 @@ import { useReactEmbeddableExecutionContext } from '../common/use_embeddable_exe import { initializeAnomalyChartsControls } from './initialize_anomaly_charts_controls'; import { LazyAnomalyChartsContainer } from './lazy_anomaly_charts_container'; import { getAnomalyChartsServiceDependencies } from './get_anomaly_charts_services_dependencies'; -import { buildDataViewPublishingApi } from '../common/anomaly_detection_embeddable'; +import { buildDataViewPublishingApi } from '../common/build_data_view_publishing_api'; export const getAnomalyChartsReactEmbeddableFactory = ( getStartServices: StartServicesAccessor diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx index 464b5bd196675..6969f43c105f3 100644 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx +++ b/x-pack/platform/plugins/shared/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.tsx @@ -41,7 +41,7 @@ import { import { HttpService } from '../../application/services/http_service'; import type { MlPluginStart, MlStartDependencies } from '../../plugin'; import { SWIM_LANE_SELECTION_TRIGGER } from '../../ui_actions'; -import { buildDataViewPublishingApi } from '../common/anomaly_detection_embeddable'; +import { buildDataViewPublishingApi } from '../common/build_data_view_publishing_api'; import { useReactEmbeddableExecutionContext } from '../common/use_embeddable_execution_context'; import { initializeSwimLaneControls } from './initialize_swim_lane_controls'; import { initializeSwimLaneDataFetcher } from './initialize_swim_lane_data_fetcher'; diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/common/anomaly_detection_embeddable.ts b/x-pack/platform/plugins/shared/ml/public/embeddables/common/anomaly_detection_embeddable.ts deleted file mode 100644 index a4e1d423f94d8..0000000000000 --- a/x-pack/platform/plugins/shared/ml/public/embeddables/common/anomaly_detection_embeddable.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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 DataView } from '@kbn/data-views-plugin/common'; -import { type DataViewsContract } from '@kbn/data-views-plugin/public'; -import type { IContainer } from '@kbn/embeddable-plugin/public'; -import { - Embeddable, - type EmbeddableInput, - type EmbeddableOutput, -} from '@kbn/embeddable-plugin/public'; -import type { PublishingSubject } from '@kbn/presentation-publishing'; -import type { Subscription } from 'rxjs'; -import { BehaviorSubject, firstValueFrom, forkJoin, from, map, switchMap } from 'rxjs'; -import { type AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; -import type { JobId } from '../../../common/types/anomaly_detection_jobs'; -import type { AnomalySwimLaneEmbeddableApi } from '../anomaly_swimlane/types'; - -export type CommonInput = { jobIds: string[] } & EmbeddableInput; - -export type CommonOutput = { indexPatterns?: DataView[] } & EmbeddableOutput; - -export const buildDataViewPublishingApi = ( - services: { anomalyDetectorService: AnomalyDetectorService; dataViewsService: DataViewsContract }, - api: Pick, - subscription: Subscription -): PublishingSubject => { - const dataViews$ = new BehaviorSubject(undefined); - - subscription.add( - api.jobIds - .pipe( - // Get job definitions - switchMap((jobIds) => services.anomalyDetectorService.getJobs$(jobIds)), - // Get unique indices from the datafeed configs - map((jobs) => [...new Set(jobs.map((j) => j.datafeed_config!.indices).flat())]), - switchMap((indices) => - forkJoin( - indices.map((indexName) => - from( - services.dataViewsService.find(`"${indexName}"`).then((r) => { - const dView = r.find((obj) => - obj.getIndexPattern().toLowerCase().includes(indexName.toLowerCase()) - ); - - return dView; - }) - ) - ) - ) - ), - map((results) => { - return results.flat().filter((dView) => dView !== undefined) as DataView[]; - }) - ) - .subscribe(dataViews$) - ); - - return dataViews$; -}; - -export abstract class AnomalyDetectionEmbeddable< - Input extends CommonInput, - Output extends CommonOutput -> extends Embeddable { - // Need to defer embeddable load in order to resolve data views - deferEmbeddableLoad = true; - - // API - public abstract jobIds: BehaviorSubject; - - protected constructor( - initialInput: Input, - private anomalyDetectorService: AnomalyDetectorService, - private dataViewsService: DataViewsContract, - parent?: IContainer - ) { - super(initialInput, {} as Output, parent); - - this.initializeOutput(initialInput).finally(() => { - this.setInitializationFinished(); - }); - } - - protected async initializeOutput(initialInput: CommonInput) { - const { jobIds } = initialInput; - - try { - const jobs = await firstValueFrom(this.anomalyDetectorService.getJobs$(jobIds)); - - // First get list of unique indices from the selected jobs - const indices = new Set(jobs.map((j) => j.datafeed_config!.indices).flat()); - // Then find the data view assuming the data view title matches the index name - const indexPatterns: Record = {}; - for (const indexName of indices) { - const response = await this.dataViewsService.find(`"${indexName}"`); - const indexPattern = response.find((obj) => - obj.getIndexPattern().toLowerCase().includes(indexName.toLowerCase()) - ); - - if (indexPattern !== undefined) { - indexPatterns[indexPattern.id!] = indexPattern; - } - } - - this.updateOutput({ - ...this.getOutput(), - indexPatterns: Object.values(indexPatterns), - }); - } catch (e) { - // Unable to find and load data view but we can ignore the error - // as we only load it to support the filter & query bar - // the visualizations should still work correctly - - // eslint-disable-next-line no-console - console.error(`Unable to load data views for ${jobIds}`, e); - } - } -} diff --git a/x-pack/platform/plugins/shared/ml/public/embeddables/common/build_data_view_publishing_api.ts b/x-pack/platform/plugins/shared/ml/public/embeddables/common/build_data_view_publishing_api.ts new file mode 100644 index 0000000000000..62f14602f3af3 --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/embeddables/common/build_data_view_publishing_api.ts @@ -0,0 +1,53 @@ +/* + * 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 DataView } from '@kbn/data-views-plugin/common'; +import { type DataViewsContract } from '@kbn/data-views-plugin/public'; +import type { PublishingSubject } from '@kbn/presentation-publishing'; +import type { Subscription } from 'rxjs'; +import { BehaviorSubject, forkJoin, from, map, switchMap } from 'rxjs'; +import { type AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; +import type { AnomalySwimLaneEmbeddableApi } from '../anomaly_swimlane/types'; + +export const buildDataViewPublishingApi = ( + services: { anomalyDetectorService: AnomalyDetectorService; dataViewsService: DataViewsContract }, + api: Pick, + subscription: Subscription +): PublishingSubject => { + const dataViews$ = new BehaviorSubject(undefined); + + subscription.add( + api.jobIds + .pipe( + // Get job definitions + switchMap((jobIds) => services.anomalyDetectorService.getJobs$(jobIds)), + // Get unique indices from the datafeed configs + map((jobs) => [...new Set(jobs.map((j) => j.datafeed_config!.indices).flat())]), + switchMap((indices) => + forkJoin( + indices.map((indexName) => + from( + services.dataViewsService.find(`"${indexName}"`).then((r) => { + const dView = r.find((obj) => + obj.getIndexPattern().toLowerCase().includes(indexName.toLowerCase()) + ); + + return dView; + }) + ) + ) + ) + ), + map((results) => { + return results.flat().filter((dView) => dView !== undefined) as DataView[]; + }) + ) + .subscribe(dataViews$) + ); + + return dataViews$; +}; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts index 0f5a12d862d7e..c8e775ab4db6c 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -7,15 +7,10 @@ import { coreMock } from '@kbn/core/public/mocks'; import { DataView } from '@kbn/data-views-plugin/common'; import { DiscoverAppLocator } from '@kbn/discover-plugin/common'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; import type { Filter, RangeFilter } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; -import { ViewMode as ViewModeType } from '@kbn/presentation-publishing'; +import { ViewMode } from '@kbn/presentation-publishing'; import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; -import { - VisualizeEmbeddableContract, - VISUALIZE_EMBEDDABLE_TYPE, -} from '@kbn/visualizations-plugin/public'; import { BehaviorSubject } from 'rxjs'; import { Params, PluginDeps } from './abstract_explore_data_action'; import { ExploreDataChartAction, ExploreDataChartActionContext } from './explore_data_chart_action'; @@ -70,18 +65,18 @@ const setup = ( }; const action = new ExploreDataChartAction(params); - const embeddable: VisualizeEmbeddableContract = { - type: VISUALIZE_EMBEDDABLE_TYPE, - dataViews: new BehaviorSubject([ + const embeddable = { + type: 'anyEmbeddable', + dataViews: new BehaviorSubject([ { id: 'index-ptr-foo', - }, + } as DataView, ]), - filters$: new BehaviorSubject([]), + filters$: new BehaviorSubject([]), parentApi: { - viewMode: new BehaviorSubject(ViewMode.VIEW), + viewMode: new BehaviorSubject('view'), }, - } as unknown as VisualizeEmbeddableContract; + }; const context = { filters, @@ -138,11 +133,11 @@ describe('"Explore underlying data" panel action', () => { embeddable.dataViews = new BehaviorSubject([ { id: 'index-ptr-foo', - }, + } as DataView, { id: 'index-ptr-bar', - }, - ] as any as DataView[]); + } as DataView, + ]); const isCompatible = await action.isCompatible(context); @@ -171,7 +166,7 @@ describe('"Explore underlying data" panel action', () => { test('returns false if dashboard is in edit mode', async () => { const { action, embeddable, context } = setup(); if (embeddable.parentApi) { - embeddable.parentApi.viewMode = new BehaviorSubject(ViewMode.EDIT); + embeddable.parentApi.viewMode = new BehaviorSubject('edit'); } const isCompatible = await action.isCompatible(context); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts index d2b84b6ddb7bf..abe0776d57f5f 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts @@ -8,14 +8,9 @@ import { coreMock } from '@kbn/core/public/mocks'; import { DataView } from '@kbn/data-views-plugin/common'; import { DiscoverAppLocator } from '@kbn/discover-plugin/common'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; -import { ViewMode as ViewModeType } from '@kbn/presentation-publishing'; +import { ViewMode } from '@kbn/presentation-publishing'; import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; -import { - VisualizeEmbeddableContract, - VISUALIZE_EMBEDDABLE_TYPE, -} from '@kbn/visualizations-plugin/public'; import { BehaviorSubject } from 'rxjs'; import { Params, PluginDeps } from './abstract_explore_data_action'; import { ExploreDataContextMenuAction } from './explore_data_context_menu_action'; @@ -60,18 +55,17 @@ const setup = () => { }; const action = new ExploreDataContextMenuAction(params); - const embeddable: VisualizeEmbeddableContract = { - type: VISUALIZE_EMBEDDABLE_TYPE, - dataViews: new BehaviorSubject([ + const embeddable = { + type: 'anyEmbeddable', + dataViews: new BehaviorSubject([ { id: 'index-ptr-foo', - }, + } as DataView, ]), parentApi: { - viewMode: new BehaviorSubject(ViewMode.VIEW), - localFilters: new BehaviorSubject([]), + viewMode: new BehaviorSubject('view'), }, - } as unknown as VisualizeEmbeddableContract; + }; const context = { embeddable, @@ -126,11 +120,11 @@ describe('"Explore underlying data" panel action', () => { embeddable.dataViews = new BehaviorSubject([ { id: 'index-ptr-foo', - }, + } as DataView, { id: 'index-ptr-bar', - }, - ] as any as DataView[]); + } as DataView, + ]); const isCompatible = await action.isCompatible(context); @@ -159,7 +153,7 @@ describe('"Explore underlying data" panel action', () => { test('returns false if dashboard is in edit mode', async () => { const { action, embeddable, context } = setup(); if (embeddable.parentApi) { - embeddable.parentApi.viewMode = new BehaviorSubject(ViewMode.EDIT); + embeddable.parentApi.viewMode = new BehaviorSubject('edit'); } const isCompatible = await action.isCompatible(context); diff --git a/x-pack/plugins/discover_enhanced/tsconfig.json b/x-pack/plugins/discover_enhanced/tsconfig.json index 6839f4c2c18e1..79dad79381e97 100644 --- a/x-pack/plugins/discover_enhanced/tsconfig.json +++ b/x-pack/plugins/discover_enhanced/tsconfig.json @@ -13,7 +13,6 @@ "@kbn/lens-plugin", "@kbn/usage-collection-plugin", "@kbn/embeddable-plugin", - "@kbn/visualizations-plugin", "@kbn/ui-actions-plugin", "@kbn/i18n", "@kbn/es-query",