From b0a8f2bc0f45efe226e35360633ecde073e508c7 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Tue, 15 Sep 2020 13:35:13 +0100 Subject: [PATCH] Implementing ReferenceOrValueEmbeddable for visualize embeddable (#76088) (#77457) * Adding dashboard as dependency to visualize * Making visualize embeddable as ReferenceOrValueEmbeddable * Adding attribute service to visualize embeddable * Binding createFromSavedObject method * Add/remove visualize embeddable from library * Removing debugger statement * Using custom save method on attribute service * Reverting to not using attribute service * Resetting flag value * Fix i18n title and update title * Using custom save method on attribute service * Fixing eslint * Using custom save method in visualize embeddable factory * Making getInputAsValueType return Promise as it should * Better error handling when saving a visualization fails Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../attribute_service/attribute_service.tsx | 8 +- src/plugins/dashboard/public/mocks.tsx | 35 +++++++ .../default_embeddable_factory_provider.ts | 10 +- src/plugins/visualizations/kibana.json | 2 +- .../create_vis_embeddable_from_object.ts | 19 +++- .../public/embeddable/visualize_embeddable.ts | 81 +++++++++++++++- .../visualize_embeddable_factory.tsx | 93 ++++++++++++++++++- src/plugins/visualizations/public/mocks.ts | 3 + src/plugins/visualizations/public/plugin.ts | 5 +- 9 files changed, 240 insertions(+), 16 deletions(-) create mode 100644 src/plugins/dashboard/public/mocks.tsx diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx index a36363d22d87da..84df05154fb630 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -57,7 +57,7 @@ export interface AttributeServiceOptions { type: string, attributes: A, savedObjectId?: string - ) => Promise<{ id: string }>; + ) => Promise<{ id?: string } | { error: Error }>; customUnwrapMethod?: (savedObject: SimpleSavedObject) => A; } @@ -124,7 +124,10 @@ export class AttributeService< newAttributes, savedObjectId ); - return { ...originalInput, savedObjectId: savedItem.id } as RefType; + if ('id' in savedItem) { + return { ...originalInput, savedObjectId: savedItem.id } as RefType; + } + return { ...originalInput } as RefType; } if (savedObjectId) { @@ -208,7 +211,6 @@ export class AttributeService< return { error }; } }; - if (saveOptions && (saveOptions as { showSaveModal: boolean }).showSaveModal) { this.showSaveModal( ; + +const createStartContract = (): DashboardStart => { + // @ts-ignore + const startContract: DashboardStart = { + getAttributeService: jest.fn(), + }; + + return startContract; +}; + +export const dashboardPluginMock = { + createStartContract, +}; 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 570a78fc41ea91..b22f16c94aff8d 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 @@ -37,11 +37,11 @@ export const defaultEmbeddableFactoryProvider = < getExplicitInput: def.getExplicitInput ? def.getExplicitInput.bind(def) : () => Promise.resolve({}), - createFromSavedObject: - def.createFromSavedObject ?? - ((savedObjectId: string, input: Partial, parent?: IContainer) => { - throw new Error(`Creation from saved object not supported by type ${def.type}`); - }), + createFromSavedObject: def.createFromSavedObject + ? def.createFromSavedObject.bind(def) + : (savedObjectId: string, input: Partial, parent?: IContainer) => { + throw new Error(`Creation from saved object not supported by type ${def.type}`); + }, create: def.create.bind(def), type: def.type, isEditable: def.isEditable.bind(def), diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index da3edfbdd3bf5a..0bd5de1d9ee15e 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector"], + "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector", "dashboard"], "requiredBundles": ["kibanaUtils", "discover", "savedObjects"] } diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index 194deef82a5f0b..b27d24d980e8d6 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -18,7 +18,13 @@ */ import { Vis } from '../types'; -import { VisualizeInput, VisualizeEmbeddable } from './visualize_embeddable'; +import { + VisualizeInput, + VisualizeEmbeddable, + VisualizeByValueInput, + VisualizeByReferenceInput, + VisualizeSavedObjectAttributes, +} from './visualize_embeddable'; import { IContainer, ErrorEmbeddable } from '../../../../plugins/embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { @@ -30,10 +36,18 @@ import { } from '../services'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; +import { SavedVisualizationsLoader } from '../saved_visualizations'; +import { AttributeService } from '../../../dashboard/public'; export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDeps) => async ( vis: Vis, input: Partial & { id: string }, + savedVisualizationsLoader?: SavedVisualizationsLoader, + attributeService?: AttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >, parent?: IContainer ): Promise => { const savedVisualizations = getSavedVisualizationsLoader(); @@ -55,6 +69,7 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe const indexPattern = vis.data.indexPattern; const indexPatterns = indexPattern ? [indexPattern] : []; const editable = getCapabilities().visualize.save as boolean; + return new VisualizeEmbeddable( getTimeFilter(), { @@ -66,6 +81,8 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe deps, }, input, + attributeService, + savedVisualizationsLoader, parent ); } catch (e) { diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index cc278a6ee9b3d8..18ae68ec40fe5c 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -20,6 +20,7 @@ import _, { get } from 'lodash'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; +import { i18n } from '@kbn/i18n'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IIndexPattern, @@ -35,6 +36,8 @@ import { Embeddable, IContainer, Adapters, + SavedObjectEmbeddableInput, + ReferenceOrValueEmbeddable, } from '../../../../plugins/embeddable/public'; import { IExpressionLoaderParams, @@ -47,6 +50,10 @@ import { getExpressions, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { TriggerId } from '../../../ui_actions/public'; +import { SavedObjectAttributes } from '../../../../core/types'; +import { AttributeService } from '../../../dashboard/public'; +import { SavedVisualizationsLoader } from '../saved_visualizations'; +import { VisSavedObject } from '../types'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -75,9 +82,19 @@ export interface VisualizeOutput extends EmbeddableOutput { visTypeName: string; } +export type VisualizeSavedObjectAttributes = SavedObjectAttributes & { + title: string; + vis?: Vis; + savedVis?: VisSavedObject; +}; +export type VisualizeByValueInput = { attributes: VisualizeSavedObjectAttributes } & VisualizeInput; +export type VisualizeByReferenceInput = SavedObjectEmbeddableInput & VisualizeInput; + type ExpressionLoader = InstanceType; -export class VisualizeEmbeddable extends Embeddable { +export class VisualizeEmbeddable + extends Embeddable + implements ReferenceOrValueEmbeddable { private handler?: ExpressionLoader; private timefilter: TimefilterContract; private timeRange?: TimeRange; @@ -93,11 +110,23 @@ export class VisualizeEmbeddable extends Embeddable; + private savedVisualizationsLoader?: SavedVisualizationsLoader; constructor( timefilter: TimefilterContract, { vis, editPath, editUrl, indexPatterns, editable, deps }: VisualizeEmbeddableConfiguration, initialInput: VisualizeInput, + attributeService?: AttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >, + savedVisualizationsLoader?: SavedVisualizationsLoader, parent?: IContainer ) { super( @@ -118,6 +147,8 @@ export class VisualizeEmbeddable extends Embeddable { + if (!this.attributeService) { + throw new Error('AttributeService must be defined for getInputAsRefType'); + } + return this.attributeService.inputIsRefType(input as VisualizeByReferenceInput); + }; + + getInputAsValueType = async (): Promise => { + const input = { + savedVis: this.vis.serialize(), + }; + if (this.getTitle()) { + input.savedVis.title = this.getTitle(); + } + delete input.savedVis.id; + return new Promise((resolve) => { + resolve({ ...(input as VisualizeByValueInput) }); + }); + }; + + getInputAsRefType = async (): Promise => { + const savedVis = await this.savedVisualizationsLoader?.get({}); + if (!savedVis) { + throw new Error('Error creating a saved vis object'); + } + if (!this.attributeService) { + throw new Error('AttributeService must be defined for getInputAsRefType'); + } + const saveModalTitle = this.getTitle() + ? this.getTitle() + : i18n.translate('visualizations.embeddable.placeholderTitle', { + defaultMessage: 'Placeholder Title', + }); + // @ts-ignore + const attributes: VisualizeSavedObjectAttributes = { + savedVis, + vis: this.vis, + title: this.vis.title, + }; + return this.attributeService.getInputAsRefType( + { + id: this.id, + attributes, + }, + { showSaveModal: true, saveModalTitle } + ); + }; } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index b81ff5c1661831..75e53e8e92dbe5 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -28,7 +28,14 @@ import { IContainer, } from '../../../embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; -import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable'; +import { + VisualizeByReferenceInput, + VisualizeByValueInput, + VisualizeEmbeddable, + VisualizeInput, + VisualizeOutput, + VisualizeSavedObjectAttributes, +} from './visualize_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { SerializedVis, Vis } from '../vis'; import { @@ -43,13 +50,16 @@ import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_obje import { StartServicesGetter } from '../../../kibana_utils/public'; import { VisualizationsStartDeps } from '../plugin'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; +import { AttributeService } from '../../../dashboard/public'; interface VisualizationAttributes extends SavedObjectAttributes { visState: string; } export interface VisualizeEmbeddableFactoryDeps { - start: StartServicesGetter>; + start: StartServicesGetter< + Pick + >; } export class VisualizeEmbeddableFactory @@ -62,6 +72,12 @@ export class VisualizeEmbeddableFactory > { 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'], @@ -105,6 +121,19 @@ export class VisualizeEmbeddableFactory return await this.deps.start().core.application.currentAppId$.pipe(first()).toPromise(); } + private async getAttributeService() { + if (!this.attributeService) { + this.attributeService = await this.deps + .start() + .plugins.dashboard.getAttributeService< + VisualizeSavedObjectAttributes, + VisualizeByValueInput, + VisualizeByReferenceInput + >(this.type, { customSaveMethod: this.onSave }); + } + return this.attributeService!; + } + public async createFromSavedObject( savedObjectId: string, input: Partial & { id: string }, @@ -117,7 +146,13 @@ export class VisualizeEmbeddableFactory const visState = convertToSerializedVis(savedObject); const vis = new Vis(savedObject.visState.type, visState); await vis.setState(visState); - return createVisEmbeddableFromObject(this.deps)(vis, input, parent); + return createVisEmbeddableFromObject(this.deps)( + vis, + input, + savedVisualizations, + await this.getAttributeService(), + parent + ); } catch (e) { console.error(e); // eslint-disable-line no-console return new ErrorEmbeddable(e, input, parent); @@ -131,7 +166,14 @@ export class VisualizeEmbeddableFactory const visState = input.savedVis; const vis = new Vis(visState.type, visState); await vis.setState(visState); - return createVisEmbeddableFromObject(this.deps)(vis, input, parent); + const savedVisualizations = getSavedVisualizationsLoader(); + return createVisEmbeddableFromObject(this.deps)( + vis, + input, + savedVisualizations, + await this.getAttributeService(), + parent + ); } else { showNewVisModal({ originatingApp: await this.getCurrentAppId(), @@ -140,4 +182,47 @@ export class VisualizeEmbeddableFactory return undefined; } } + + private async onSave( + type: string, + 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, + }; + savedVis.title = title; + savedVis.copyOnSave = false; + savedVis.description = ''; + savedVis.searchSourceFields = visObj?.data.searchSource?.getSerializedFields(); + 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 id = await savedVis.save(saveOptions); + if (!id || id === '') { + throw new Error( + i18n.translate('visualizations.savingVisualizationFailed.errorMsg', { + defaultMessage: 'Saving a visualization failed', + }) + ); + } + return { id }; + } catch (error) { + throw error; + } + } } diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index e0ec4801b3caf7..646acc49a6a832 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -27,6 +27,7 @@ import { dataPluginMock } from '../../../plugins/data/public/mocks'; import { usageCollectionPluginMock } from '../../../plugins/usage_collection/public/mocks'; import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; +import { dashboardPluginMock } from '../../../plugins/dashboard/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), @@ -69,6 +70,8 @@ const createInstance = async () => { uiActions: uiActionsPluginMock.createStartContract(), application: applicationServiceMock.createStartContract(), embeddable: embeddablePluginMock.createStartContract(), + dashboard: dashboardPluginMock.createStartContract(), + getAttributeService: jest.fn(), }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 3546fa40564915..0ba80887b513f2 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -76,6 +76,7 @@ import { convertToSerializedVis, } from './saved_visualizations/_saved_vis'; import { createSavedSearchesLoader } from '../../discover/public'; +import { DashboardStart } from '../../dashboard/public'; /** * Interface for this plugin's returned setup/start contracts. @@ -109,6 +110,8 @@ export interface VisualizationsStartDeps { inspector: InspectorStart; uiActions: UiActionsStart; application: ApplicationStart; + dashboard: DashboardStart; + getAttributeService: DashboardStart['getAttributeService']; } /** @@ -155,7 +158,7 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions, embeddable }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable, dashboard }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n);