elastic / kibana

Your window into the Elastic Stack
https://www.elastic.co/products/kibana
Other
19.63k stars 8.22k forks source link

Embeddables API #19875

Closed stacey-gammon closed 5 years ago

stacey-gammon commented 6 years ago

Embeddables

Goal:

Related:

Current infrastructure

embeddable vs visualize loader

Future infrastructure

embeddable vs visualize loader 1

API Overview

Embeddable

export interface IEmbeddable<I, O> {
  /**
   * 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?: Container;

  /**
   * 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.
   **/
  readonly type: string;

  /**
   * A unique identifier for this embeddable. Mainly only used by containers to map their
   * Panel States to a child embeddable instance.
   **/
  readonly id: string;

  /**
   * 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:
   * - Can be updated externally
   * - Can change multiple times for a single embeddable instance.
   * Examples: title, pie slice colors, custom search columns and sort order.
   **/
  getInput(): Readonly<I>;

  /**
   * Output state is:
   * - State that should not change once the embeddable is instantiated, or
   *  - State that is derived from the input state, or
   * - State that only the embeddable instance itself knows about, or the factory.
   * Examples: editUrl, title taken from a saved object, if your input state was first name and
   *   last name, your output state could be greeting.
   **/
  getOutput(): Readonly<O>;

  /**
   * Updates input state with the given changes.
   * @param changes
   */
  updateInput(changes: Partial<I>): void;

  /**
   * Returns an observable which will be notified when input state changes.
   */
  getInput$(): Readonly<Observable<I>>;

  /**
   * Returns an observable which will be notified when output state changes.
   */
  getOutput$(): Readonly<Observable<O>>;

  /**
   * Returns the title of this embeddable.
   */
  getTitle(): string | undefined;

  /**
   * Renders the embeddable at the given node.
   * @param domNode
   */
  render(domNode: HTMLElement | Element): void;

  /**
   * Renders the embeddable at the given node, inside a panel chrome, with
   * space for a title and a context menu that will display available
   * action functionality.
   * @param domNode
   */
  renderInPanel(node: HTMLElement | Element): void;

  /**
   * An embeddable can return inspector adapters if it want the inspector to be
   * available via the context menu of that panel.
   * @return Inspector adapters that will be used to open an inspector for.
   */
  getInspectorAdapters(): Adapters | undefined;

  /**
   * Cleans up subscriptions, destroy nodes mounted from calls to render.
   */
  destroy(): void;
}

Container.ts


export interface IContainer<
  I extends ContainerInput = ContainerInput,
  O extends ContainerOutput = ContainerOutput
> extends IEmbeddable<I, O> {
  readonly embeddableFactories: EmbeddableFactoryRegistry;

  /**
   * 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<EEI extends EmbeddableInput>(id: string): EEI;

  /**
   * Changes the input for a given child. Note, this will override any inherited state taken from
   * the container itself.
   * @param id
   * @param changes
   */
  updateInputForChild<EEI extends EmbeddableInput>(id: string, changes: Partial<EEI>): void;

  /**
   * Returns the child embeddable with the given id.
   * @param id
   */
  getChild<E extends Embeddable<EmbeddableInput> = Embeddable<EmbeddableInput>>(id: string): E;

  /**
   * Removes the embeddable with the given id.
   * @param embeddableId
   */
  removeEmbeddable(embeddableId: string): void;

  /**
   * Adds a new embeddable that is backed off of a saved object.
   */
  addSavedObjectEmbeddable<
    EEI extends EmbeddableInput = EmbeddableInput,
    E extends Embeddable<EEI> = Embeddable<EEI>
  >(
    type: string,
    savedObjectId: string
  ): Promise<E | ErrorEmbeddable>;

  /**
   * 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<EEI, EEO> = Embeddable<EEI, EEO>
  >(
    type: string,
    explicitInput: Partial<EEI>
  ): Promise<E | ErrorEmbeddable>;
}

Action API

export interface ExecuteActionContext<
  E extends Embeddable = Embeddable,
  C extends Container = Container,
  AC extends {} = {}
> {
  embeddable: E;
  container?: C;
  triggerContext?: AC;
}

export interface ActionContext<E extends Embeddable = Embeddable, C extends Container = Container> {
  embeddable: E;
  container?: C;
}

export abstract class Action<
  E extends Embeddable = Embeddable,
  C extends Container = Container,
  T extends {} = {}
> {
  /**
   * Determined the order when there is more than one action matched to a trigger.
   * Higher numbers are displayed first.
   */
  public priority: number = 0;

  constructor(public readonly id: string) {}

  /**
   * Optional icon that can be displayed along with the title.
   */
  public getIcon(context: ActionContext): EuiContextMenuItemIcon | undefined {
    return undefined;
  }

  /**
   * Returns a title to be displayed to the user.
   * @param context
   */
  public abstract getTitle(context: ActionContext): string;

  /**
   * Returns a promise that resolves to true if this action is compatible given the context,
   * otherwise resolves to false.
   */
  public isCompatible(context: ActionContext): Promise<boolean> {
    return Promise.resolve(true);
  }

  /**
   * Executes the action.
   */
  public abstract execute(context: ExecuteActionContext<E, C, T>): void;
}

Trigger, the most simple object:

export interface Trigger {
  id: string;
  title: string;
  description?: string;
  actionIds: string[];
}
kriston13 commented 6 years ago

Hi Stacey,

I was asking on the other forums about being able to specify an onclick event for various sorts of visualisations, because that exposes a new level of use cases & benefits from the visualisation tools. i.e. "take action". It's not enough anymore to just see that something is happening, the preference is to be able to take action and to put something in place to handle that, or report on it, or modify its future behaviour, all from within Kibana and/or its apps, OR the places that the visualisations are embedded into. I believe that your comments regarding

Need fleshed out support for pluggable panel actions that also speak the embeddable language

is referring to this same behaviour that I'm thinking of.

I'd also like to propose that prior to the full rearchitecture you've described above, that perhaps a smaller incremental step, is to expose a postMessage() function for visualisations (via a checkbox perhaps), so that when the user clicks on it, the function will take the information regarding that specific clicked element, and send it in the postMessage(). This will then allow some additional capabilities within the current embedded iframe approach.

Thoughts?

stacey-gammon commented 6 years ago

Hi @kriston13, sorry for the delayed response.

I'm pretty sure that we are actively working towards a solution for your use case. That is, a very flexible system for exposing actions from various events on different embeddables. Though getting a full plan in place for that is not something that will happen in the short term.

We do already have pluggable panel actions in place, maybe that is something you can look at? Nothing is actually using it at the moment, minus a test to make sure the interface works - https://github.com/elastic/kibana/tree/master/test/plugin_functional/plugins/sample_panel_action. We haven't broadcast it much because until we have a few solid use cases, the interface is probably apt to change quite a bit.

The only problem right now is that there is no additional context passed to the action - like an element that was just clicked on. We do want to think that through more too in order to support #12560

I don't really see us adding anything in the short term though, until we can spend more time understanding the longer term picture.

chrisdavies commented 5 years ago

Not sure where to comment on this, but I read through the slides, and have started thinking about this a bit.

One thing I want to point out is that I think that the thing we are calling "trigger" should be called "event", e.g. most developers would think of a "click event" rather than a "click trigger" and the thing we're calling "event" would be an "event listener" or something.

stacey-gammon commented 5 years ago

@chrisdavies good point! I'm down with changing to that terminology (though I think I will leave the current slides alone because the recorded demos use that terminology). Moving forward though, it makes sense to me!