"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.Container = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _uuid = require("uuid"); var _lodash = require("lodash"); var _rxjs = require("rxjs"); var _operators = require("rxjs/operators"); var _fastDeepEqual = _interopRequireDefault(require("fast-deep-equal")); var _embeddables = require("../embeddables"); var _errors = require("../errors"); var _saved_object_embeddable = require("../../../common/lib/saved_object_embeddable"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0 and the Server Side Public License, v 1; you may not use this file except * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ const getKeys = o => Object.keys(o); class Container extends _embeddables.Embeddable { constructor(input, output, getFactory, parent, settings) { super(input, output, parent); (0, _defineProperty2.default)(this, "isContainer", true); (0, _defineProperty2.default)(this, "children", {}); (0, _defineProperty2.default)(this, "subscription", void 0); (0, _defineProperty2.default)(this, "anyChildOutputChange$", void 0); this.getFactory = getFactory; 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 === null || settings === void 0 ? void 0 : settings.initializeSequentially) || (settings === null || settings === void 0 ? void 0 : settings.childIdInitializeOrder)); const init$ = this.getInput$().pipe((0, _operators.take)(1), (0, _operators.mergeMap)(async currentInput => { 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((0, _operators.pairwise)()); this.subscription = init$.pipe((0, _operators.combineLatestWith)(update$)).subscribe(([_, [{ panels: prevPanels }, { panels: currentPanels }]]) => { this.maybeUpdateChildren(currentPanels, prevPanels); }); this.anyChildOutputChange$ = this.getOutput$().pipe((0, _operators.map)(() => this.getChildIds()), (0, _operators.distinctUntilChanged)(_fastDeepEqual.default), // children may change, so make sure we subscribe/unsubscribe with switchMap (0, _operators.switchMap)(newChildIds => (0, _rxjs.merge)(...newChildIds.map(childId => this.getChild(childId).getOutput$().pipe( // Embeddables often throw errors into their output streams. (0, _operators.catchError)(() => _rxjs.EMPTY), (0, _operators.map)(() => childId)))))); } setChildLoaded(embeddable) { // 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; } this.children[embeddable.id] = embeddable; this.updateOutput({ embeddableLoaded: { ...this.output.embeddableLoaded, [embeddable.id]: true } }); } updateInputForChild(id, changes) { if (!this.input.panels[id]) { throw new _errors.PanelNotFoundError(); } const panels = { panels: { ...this.input.panels, [id]: { ...this.input.panels[id], explicitInput: { ...this.input.panels[id].explicitInput, ...changes } } } }; this.updateInput(panels); } reload() { Object.values(this.children).forEach(child => child.reload()); } async addNewEmbeddable(type, explicitInput) { const factory = this.getFactory(type); if (!factory) { throw new _errors.EmbeddableFactoryNotFoundError(type); } const panelState = this.createNewPanelState(factory, explicitInput); return this.createAndSaveEmbeddable(type, panelState); } async replaceEmbeddable(id, newExplicitInput, newType) { if (!this.input.panels[id]) { throw new _errors.PanelNotFoundError(); } if (newType && newType !== this.input.panels[id].type) { const factory = this.getFactory(newType); if (!factory) { throw new _errors.EmbeddableFactoryNotFoundError(newType); } this.updateInput({ panels: { ...this.input.panels, [id]: { ...this.input.panels[id], explicitInput: { ...newExplicitInput, id }, type: newType } } }); } else { this.updateInputForChild(id, newExplicitInput); } await this.untilEmbeddableLoaded(id); } removeEmbeddable(embeddableId) { // 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 }); } /** * 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. */ onRemoveEmbeddable(embeddableId) { const panels = { ...this.input.panels }; delete panels[embeddableId]; return panels; } getChildIds() { return Object.keys(this.children); } getChild(id) { return this.children[id]; } getInputForChild(embeddableId) { const containerInput = this.getInheritedInput(embeddableId); const panelState = this.getPanelState(embeddableId); const explicitInput = panelState.explicitInput; const explicitFiltered = {}; 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. }; } getAnyChildOutputChange$() { return this.anyChildOutputChange$; } destroy() { var _this$subscription; super.destroy(); Object.values(this.children).forEach(child => child.destroy()); (_this$subscription = this.subscription) === null || _this$subscription === void 0 ? void 0 : _this$subscription.unsubscribe(); } async untilEmbeddableLoaded(id) { if (!this.input.panels[id]) { throw new _errors.PanelNotFoundError(); } if (this.output.embeddableLoaded[id]) { return this.children[id]; } return new Promise((resolve, reject) => { const subscription = (0, _rxjs.merge)(this.getOutput$(), this.getInput$()).subscribe(() => { if (this.output.embeddableLoaded[id]) { subscription.unsubscribe(); resolve(this.children[id]); } // 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); } }); }); } async getExplicitInputIsEqual(lastInput) { const { panels: lastPanels, ...restOfLastInput } = lastInput; const { panels: currentPanels, ...restOfCurrentInput } = this.getInput(); const otherInputIsEqual = (0, _lodash.isEqual)(restOfLastInput, restOfCurrentInput); if (!otherInputIsEqual) return false; const embeddableIdsA = Object.keys(lastPanels); const embeddableIdsB = Object.keys(currentPanels); if (embeddableIdsA.length !== embeddableIdsB.length || (0, _lodash.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 ((0, _embeddables.isErrorEmbeddable)(currentEmbeddable)) continue; if (!(await currentEmbeddable.getExplicitInputIsEqual(lastPanelInput))) { return false; } } return true; } createNewPanelState(factory, partial = {}) { const embeddableId = partial.id || (0, _uuid.v4)(); const explicitInput = this.createNewExplicitEmbeddableInput(embeddableId, factory, partial); return { type: factory.type, explicitInput: { ...explicitInput, id: embeddableId } }; } getPanelState(embeddableId) { if (this.input.panels[embeddableId] === undefined) { throw new _errors.PanelNotFoundError(); } const panelState = this.input.panels[embeddableId]; return 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. */ async initializeChildEmbeddables(initialInput, initializeSettings) { let initializeOrder = Object.keys(initialInput.panels); if (initializeSettings !== null && initializeSettings !== void 0 && 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 !== null && initializeSettings !== void 0 && initializeSettings.initializeSequentially) { const embeddable = await this.onPanelAdded(initialInput.panels[id]); if (embeddable && !(0, _embeddables.isErrorEmbeddable)(embeddable)) { await this.untilEmbeddableLoaded(id); } } else { this.onPanelAdded(initialInput.panels[id]); } } } async createAndSaveEmbeddable(type, panelState) { this.updateInput({ panels: { ...this.input.panels, [panelState.explicitInput.id]: panelState } }); return await this.untilEmbeddableLoaded(panelState.explicitInput.id); } createNewExplicitEmbeddableInput(id, factory, partial = {}) { const inheritedInput = this.getInheritedInput(id); const defaults = factory.getDefaultInput(partial); // Container input overrides defaults. const explicitInput = 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; } onPanelRemoved(id) { // Clean up const embeddable = this.getChild(id); if (embeddable) { embeddable.destroy(); // Remove references. delete this.children[id]; } this.updateOutput({ embeddableLoaded: { ...this.output.embeddableLoaded, [id]: undefined } }); } async onPanelAdded(panel) { this.updateOutput({ embeddableLoaded: { ...this.output.embeddableLoaded, [panel.explicitInput.id]: false } }); let embeddable; const inputForChild = this.getInputForChild(panel.explicitInput.id); try { const factory = this.getFactory(panel.type); if (!factory) { throw new _errors.EmbeddableFactoryNotFoundError(panel.type); } // TODO: lets get rid of this distinction with factories, I don't think it will be needed after this change. embeddable = (0, _saved_object_embeddable.isSavedObjectEmbeddableInput)(inputForChild) ? await factory.createFromSavedObject(inputForChild.savedObjectId, inputForChild, this) : await factory.create(inputForChild, this); } catch (e) { embeddable = new _embeddables.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; } panelHasChanged(currentPanel, prevPanel) { if (currentPanel.type !== prevPanel.type) { return true; } } maybeUpdateChildren(currentPanels, prevPanels) { 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]); } } }); } } exports.Container = Container;