diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx
index a36363d22d87d..84df05154fb63 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 570a78fc41ea9..b22f16c94aff8 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 da3edfbdd3bf5..0bd5de1d9ee15 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 194deef82a5f0..b27d24d980e8d 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 cc278a6ee9b3d..18ae68ec40fe5 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 b81ff5c166183..75e53e8e92dbe 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 e0ec4801b3caf..646acc49a6a83 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 3546fa4056491..0ba80887b513f 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);