diff --git a/src/plugins/visualizations/public/content_management/visualization_client.ts b/src/plugins/visualizations/public/content_management/visualization_client.ts index 38eec4141a045..957c9db39a9c4 100644 --- a/src/plugins/visualizations/public/content_management/visualization_client.ts +++ b/src/plugins/visualizations/public/content_management/visualization_client.ts @@ -66,6 +66,13 @@ const deleteVisualization = async (id: string) => { }; const search = async (query: SearchQuery = {}, options?: VisualizationSearchQuery) => { + if (options && options.types && options.types.length > 1) { + const { types } = options; + return getContentManagement().client.mSearch({ + contentTypes: types.map((type) => ({ contentTypeId: type })), + query, + }); + } return getContentManagement().client.search({ contentTypeId: 'visualization', query, diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index b34cfd1c9a622..88e3f6e5fbf58 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -24,7 +24,15 @@ export { getVisSchemas } from './vis_schemas'; /** @public types */ export type { VisualizationsSetup, VisualizationsStart }; export { VisGroups } from './vis_types/vis_groups_enum'; -export type { BaseVisType, VisTypeAlias, VisTypeDefinition, Schema, ISchemas } from './vis_types'; +export type { + BaseVisType, + VisTypeAlias, + VisTypeDefinition, + Schema, + ISchemas, + VisualizationClient, + SerializableAttributes, +} from './vis_types'; export type { Vis, SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index d2805b43ed468..cd840302ff01d 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -333,6 +333,7 @@ export class VisualizationsPlugin unifiedSearch: pluginsStart.unifiedSearch, serverless: pluginsStart.serverless, noDataPage: pluginsStart.noDataPage, + contentManagement: pluginsStart.contentManagement, }; params.element.classList.add('visAppWrapper'); diff --git a/src/plugins/visualizations/public/utils/saved_objects_utils/update_basic_attributes.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/update_basic_attributes.ts index ca6158422f58a..417b3a52b5bed 100644 --- a/src/plugins/visualizations/public/utils/saved_objects_utils/update_basic_attributes.ts +++ b/src/plugins/visualizations/public/utils/saved_objects_utils/update_basic_attributes.ts @@ -9,12 +9,42 @@ import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { OverlayStart } from '@kbn/core-overlays-browser'; +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import { extractReferences } from '../saved_visualization_references'; import { visualizationsClient } from '../../content_management'; +import { TypesStart } from '../../vis_types'; interface UpdateBasicSoAttributesDependencies { savedObjectsTagging?: SavedObjectsTaggingApi; overlays: OverlayStart; + typesService: TypesStart; + contentManagement: ContentManagementPublicStart; +} + +function getClientForType( + type: string, + typesService: TypesStart, + contentManagement: ContentManagementPublicStart +) { + const visAliases = typesService.getAliases(); + return ( + visAliases + .find((v) => v.appExtensions?.visualizations.docTypes.includes(type)) + ?.appExtensions?.visualizations.client(contentManagement) || visualizationsClient + ); +} + +function getAdditionalOptionsForUpdate( + type: string, + typesService: TypesStart, + method: 'update' | 'create' +) { + const visAliases = typesService.getAliases(); + const aliasType = visAliases.find((v) => v.appExtensions?.visualizations.docTypes.includes(type)); + if (!aliasType) { + return { overwrite: true }; + } + return aliasType?.appExtensions?.visualizations?.clientOptions?.[method]; } export const updateBasicSoAttributes = async ( @@ -27,7 +57,9 @@ export const updateBasicSoAttributes = async ( }, dependencies: UpdateBasicSoAttributesDependencies ) => { - const so = await visualizationsClient.get(soId); + const client = getClientForType(type, dependencies.typesService, dependencies.contentManagement); + + const so = await client.get(soId); const extractedReferences = extractReferences({ attributes: so.item.attributes, references: so.item.references, @@ -48,14 +80,14 @@ export const updateBasicSoAttributes = async ( ); } - return await visualizationsClient.update({ + return await client.update({ id: soId, data: { ...attributes, }, options: { - overwrite: true, references, + ...getAdditionalOptionsForUpdate(type, dependencies.typesService, 'update'), }, }); }; diff --git a/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts b/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts index db19f6fc8aaa1..8945da771db7f 100644 --- a/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts +++ b/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts @@ -12,17 +12,17 @@ import { injectSearchSourceReferences, SerializedSearchSourceFields, } from '@kbn/data-plugin/public'; -import { SerializableRecord } from '@kbn/utility-types'; import { SavedVisState, VisSavedObject } from '../../types'; import { extractTimeSeriesReferences, injectTimeSeriesReferences } from './timeseries_references'; import { extractControlsReferences, injectControlsReferences } from './controls_references'; +import type { SerializableAttributes } from '../../vis_types/vis_type_alias_registry'; export function extractReferences({ attributes, references = [], }: { - attributes: SerializableRecord; + attributes: SerializableAttributes; references: SavedObjectReference[]; }) { const updatedAttributes = { ...attributes }; diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts index d5b26fe455ac6..01475fe483c80 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts @@ -73,6 +73,7 @@ jest.mock('../services', () => ({ update: mockUpdateContent, get: mockGetContent, search: mockFindContent, + mSearch: mockFindContent, }, })), })); @@ -358,10 +359,11 @@ describe('saved_visualize_utils', () => { expect(mockFindContent.mock.calls).toMatchObject([ [ { - options: { - types: ['bazdoc', 'etc', 'visualization'], - searchFields: ['baz', 'bing', 'title^3', 'description'], - }, + contentTypes: [ + { contentTypeId: 'bazdoc' }, + { contentTypeId: 'etc' }, + { contentTypeId: 'visualization' }, + ], }, ], ]); @@ -395,10 +397,12 @@ describe('saved_visualize_utils', () => { expect(mockFindContent.mock.calls).toMatchObject([ [ { - options: { - types: ['bazdoc', 'bar', 'visualization', 'foo'], - searchFields: ['baz', 'bing', 'barfield', 'foofield', 'title^3', 'description'], - }, + contentTypes: [ + { contentTypeId: 'bazdoc' }, + { contentTypeId: 'bar' }, + { contentTypeId: 'visualization' }, + { contentTypeId: 'foo' }, + ], }, ], ]); diff --git a/src/plugins/visualizations/public/vis_types/index.ts b/src/plugins/visualizations/public/vis_types/index.ts index 365f0d51bf4f3..d595586df2d47 100644 --- a/src/plugins/visualizations/public/vis_types/index.ts +++ b/src/plugins/visualizations/public/vis_types/index.ts @@ -11,3 +11,4 @@ export { Schemas } from './schemas'; export { VisGroups } from './vis_groups_enum'; export { BaseVisType } from './base_vis_type'; export type { VisTypeDefinition, ISchemas, Schema } from './types'; +export type { VisualizationClient, SerializableAttributes } from './vis_type_alias_registry'; diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index 61e36c931390e..2a46b28f06dd9 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -6,6 +6,13 @@ * Side Public License, v 1. */ +import { SearchQuery } from '@kbn/content-management-plugin/common'; +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import { + ContentManagementCrudTypes, + SavedObjectCreateOptions, + SavedObjectUpdateOptions, +} from '@kbn/content-management-utils'; import type { SimpleSavedObject } from '@kbn/core/public'; import { BaseVisType } from './base_vis_type'; @@ -27,9 +34,54 @@ export interface VisualizationListItem { type?: BaseVisType | string; } +export interface SerializableAttributes { + [key: string]: unknown; +} + +export type GenericVisualizationCrudTypes< + ContentType extends string, + Attr extends SerializableAttributes +> = ContentManagementCrudTypes< + ContentType, + Attr, + Pick, + Pick, + object +>; + +export interface VisualizationClient< + ContentType extends string = string, + Attr extends SerializableAttributes = SerializableAttributes +> { + get: (id: string) => Promise['GetOut']>; + create: ( + visualization: Omit< + GenericVisualizationCrudTypes['CreateIn'], + 'contentTypeId' + > + ) => Promise['CreateOut']>; + update: ( + visualization: Omit< + GenericVisualizationCrudTypes['UpdateIn'], + 'contentTypeId' + > + ) => Promise['UpdateOut']>; + delete: (id: string) => Promise['DeleteOut']>; + search: ( + query: SearchQuery, + options?: object + ) => Promise['SearchOut']>; +} + export interface VisualizationsAppExtension { docTypes: string[]; searchFields?: string[]; + /** let each visualization client pass its own custom options if required */ + clientOptions?: { + update?: { overwrite?: boolean; [otherOption: string]: unknown }; + create?: { [otherOption: string]: unknown }; + }; + client: (contentManagement: ContentManagementPublicStart) => VisualizationClient; toListItem: (savedObject: SimpleSavedObject) => VisualizationListItem; } diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx index 30bb05c7b43d6..866acf33c1592 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx @@ -109,6 +109,7 @@ const useTableListViewProps = ( overlays, toastNotifications, visualizeCapabilities, + contentManagement, }, } = useKibana(); @@ -176,11 +177,16 @@ const useTableListViewProps = ( description: args.description ?? '', tags: args.tags, }, - { overlays, savedObjectsTagging } + { + overlays, + savedObjectsTagging, + typesService: getTypes(), + contentManagement, + } ); } }, - [overlays, savedObjectsTagging] + [overlays, savedObjectsTagging, contentManagement] ); const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo( diff --git a/src/plugins/visualizations/public/visualize_app/types.ts b/src/plugins/visualizations/public/visualize_app/types.ts index 77f743aaeef77..abc55e3e671fe 100644 --- a/src/plugins/visualizations/public/visualize_app/types.ts +++ b/src/plugins/visualizations/public/visualize_app/types.ts @@ -42,6 +42,7 @@ import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plug import type { SavedSearch, SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import type { Vis, VisualizeEmbeddableContract, @@ -119,6 +120,7 @@ export interface VisualizeServices extends CoreStart { unifiedSearch: UnifiedSearchPublicPluginStart; serverless?: ServerlessPluginStart; noDataPage?: NoDataPagePluginStart; + contentManagement: ContentManagementPublicStart; } export interface VisInstance { diff --git a/test/functional/apps/dashboard/group4/dashboard_listing.ts b/test/functional/apps/dashboard/group4/dashboard_listing.ts index d3cde4e185b9f..ed8cc60cb5884 100644 --- a/test/functional/apps/dashboard/group4/dashboard_listing.ts +++ b/test/functional/apps/dashboard/group4/dashboard_listing.ts @@ -14,8 +14,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['dashboard', 'header', 'common']); const browser = getService('browser'); const listingTable = getService('listingTable'); - const testSubjects = getService('testSubjects'); - const retry = getService('retry'); const dashboardAddPanel = getService('dashboardAddPanel'); describe('dashboard listing page', function describeIndexTests() { @@ -217,12 +215,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.gotoDashboardLandingPage(); await listingTable.searchForItemWithName(`${dashboardName}-editMetaData`); - await testSubjects.click('inspect-action'); - await testSubjects.setValue('nameInput', 'new title'); - await testSubjects.setValue('descriptionInput', 'new description'); - await retry.try(async () => { - await testSubjects.click('saveButton'); - await testSubjects.missingOrFail('flyoutTitle'); + await listingTable.inspectVisualization(); + await listingTable.editVisualizationDetails({ + title: 'new title', + description: 'new description', }); await listingTable.searchAndExpectItemsCount('dashboard', 'new title', 1); diff --git a/test/functional/apps/visualize/group3/_visualize_listing.ts b/test/functional/apps/visualize/group3/_visualize_listing.ts index ad370939f2260..7adc5e088ce7c 100644 --- a/test/functional/apps/visualize/group3/_visualize_listing.ts +++ b/test/functional/apps/visualize/group3/_visualize_listing.ts @@ -84,5 +84,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await listingTable.expectItemsCount('visualize', 0); }); }); + + describe('Edit', () => { + before(async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + }); + + it('should edit the title and description of a visualization', async () => { + await listingTable.searchForItemWithName('Hello'); + await listingTable.inspectVisualization(); + await listingTable.editVisualizationDetails({ + title: 'new title', + description: 'new description', + }); + await listingTable.searchForItemWithName('new title'); + await listingTable.expectItemsCount('visualize', 1); + }); + }); }); } diff --git a/test/functional/services/listing_table.ts b/test/functional/services/listing_table.ts index 7c2ecbbda89fa..71d71a157ceb9 100644 --- a/test/functional/services/listing_table.ts +++ b/test/functional/services/listing_table.ts @@ -154,6 +154,35 @@ export class ListingTableService extends FtrService { return visualizationNames; } + /** + * Open the inspect flyout + */ + public async inspectVisualization(index: number = 0) { + const inspectButtons = await this.testSubjects.findAll('inspect-action'); + await inspectButtons[index].click(); + } + + /** + * Edit Visualization title and description in the flyout + */ + public async editVisualizationDetails( + { title, description }: { title?: string; description?: string } = {}, + shouldSave: boolean = true + ) { + if (title) { + await this.testSubjects.setValue('nameInput', title); + } + if (description) { + await this.testSubjects.setValue('descriptionInput', description); + } + if (shouldSave) { + await this.retry.try(async () => { + await this.testSubjects.click('saveButton'); + await this.testSubjects.missingOrFail('flyoutTitle'); + }); + } + } + /** * Returns items count on landing page */ diff --git a/x-pack/plugins/lens/common/content_management/index.ts b/x-pack/plugins/lens/common/content_management/index.ts index 368a00a762ef1..f9f0280d7801e 100644 --- a/x-pack/plugins/lens/common/content_management/index.ts +++ b/x-pack/plugins/lens/common/content_management/index.ts @@ -26,6 +26,7 @@ export type { LensSearchIn, LensSearchOut, LensSearchQuery, + LensCrudTypes, } from './latest'; export * as LensV1 from './v1'; diff --git a/x-pack/plugins/lens/common/content_management/v1/index.ts b/x-pack/plugins/lens/common/content_management/v1/index.ts index 9c792d1e854b7..9e07159b8a5f5 100644 --- a/x-pack/plugins/lens/common/content_management/v1/index.ts +++ b/x-pack/plugins/lens/common/content_management/v1/index.ts @@ -22,5 +22,5 @@ export type { LensSearchIn, LensSearchOut, LensSearchQuery, - Reference, + LensCrudTypes, } from './types'; diff --git a/x-pack/plugins/lens/common/content_management/v1/types.ts b/x-pack/plugins/lens/common/content_management/v1/types.ts index 1db27403356db..d722936cdbfa6 100644 --- a/x-pack/plugins/lens/common/content_management/v1/types.ts +++ b/x-pack/plugins/lens/common/content_management/v1/types.ts @@ -5,20 +5,10 @@ * 2.0. */ -import { - GetIn, - CreateIn, - SearchIn, - UpdateIn, - DeleteIn, - DeleteResult, - SearchResult, - GetResult, - CreateResult, - UpdateResult, -} from '@kbn/content-management-plugin/common'; - -import { LensContentType } from '../types'; +import type { UpdateIn } from '@kbn/content-management-plugin/common'; +import type { ContentManagementCrudTypes } from '@kbn/content-management-utils'; + +import type { LensContentType } from '../types'; export interface Reference { type: string; @@ -26,6 +16,22 @@ export interface Reference { name: string; } +export interface CreateOptions { + /** If a document with the given `id` already exists, overwrite it's contents (default=false). */ + overwrite?: boolean; + /** Array of referenced saved objects. */ + references?: Reference[]; +} + +export interface UpdateOptions { + /** Array of referenced saved objects. */ + references?: Reference[]; +} + +export interface LensSearchQuery { + searchFields?: string[]; +} + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type LensSavedObjectAttributes = { title: string; @@ -34,78 +40,41 @@ export type LensSavedObjectAttributes = { state?: unknown; }; -export interface LensSavedObject { - id: string; - type: string; - version?: string; - updatedAt?: string; - createdAt?: string; - attributes: LensSavedObjectAttributes; - references: Reference[]; - namespaces?: string[]; - originId?: string; - error?: { - error: string; - message: string; - statusCode: number; - metadata?: Record; - }; -} +// Need to handle update in Lens in a bit different way +export type LensCrudTypes = Omit< + ContentManagementCrudTypes< + LensContentType, + LensSavedObjectAttributes, + CreateOptions, + UpdateOptions, + LensSearchQuery + >, + 'UpdateIn' +> & { UpdateIn: UpdateIn }; + +export type LensSavedObject = LensCrudTypes['Item']; +export type PartialLensSavedObject = LensCrudTypes['PartialItem']; -export type PartialLensSavedObject = Omit & { - attributes: Partial; - references: Reference[] | undefined; -}; // ----------- GET -------------- -export type LensGetIn = GetIn; +export type LensGetIn = LensCrudTypes['GetIn']; -export type LensGetOut = GetResult< - LensSavedObject, - { - outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; - aliasTargetId?: string; - aliasPurpose?: 'savedObjectConversion' | 'savedObjectImport'; - } ->; +export type LensGetOut = LensCrudTypes['GetOut']; // ----------- CREATE -------------- -export interface CreateOptions { - /** If a document with the given `id` already exists, overwrite it's contents (default=false). */ - overwrite?: boolean; - /** Array of referenced saved objects. */ - references?: Reference[]; -} - -export type LensCreateIn = CreateIn; - -export type LensCreateOut = CreateResult; +export type LensCreateIn = LensCrudTypes['CreateIn']; +export type LensCreateOut = LensCrudTypes['CreateOut']; // ----------- UPDATE -------------- -export interface UpdateOptions { - /** Array of referenced saved objects. */ - references?: Reference[]; -} - -export type LensUpdateIn = UpdateIn; - -export type LensUpdateOut = UpdateResult; - +export type LensUpdateIn = LensCrudTypes['UpdateIn']; +export type LensUpdateOut = LensCrudTypes['UpdateOut']; // ----------- DELETE -------------- -export type LensDeleteIn = DeleteIn; - -export type LensDeleteOut = DeleteResult; - +export type LensDeleteIn = LensCrudTypes['DeleteIn']; +export type LensDeleteOut = LensCrudTypes['DeleteOut']; // ----------- SEARCH -------------- -export interface LensSearchQuery { - types?: string[]; - searchFields?: string[]; -} - -export type LensSearchIn = SearchIn; - -export type LensSearchOut = SearchResult; +export type LensSearchIn = LensCrudTypes['SearchIn']; +export type LensSearchOut = LensCrudTypes['SearchOut']; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 3f6009659199c..7531dbc35c0f1 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -129,7 +129,7 @@ export async function getLensServices( settings: coreStart.settings, application: coreStart.application, notifications: coreStart.notifications, - savedObjectStore: new SavedObjectIndexStore(startDependencies.contentManagement.client), + savedObjectStore: new SavedObjectIndexStore(startDependencies.contentManagement), presentationUtil: startDependencies.presentationUtil, dataViewEditor: startDependencies.dataViewEditor, dataViewFieldEditor: startDependencies.dataViewFieldEditor, diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts index badab664d05d0..045fee2394138 100644 --- a/x-pack/plugins/lens/public/lens_attribute_service.ts +++ b/x-pack/plugins/lens/public/lens_attribute_service.ts @@ -30,7 +30,7 @@ export function getLensAttributeService( core: CoreStart, startDependencies: LensPluginStartDependencies ): LensAttributeService { - const savedObjectStore = new SavedObjectIndexStore(startDependencies.contentManagement.client); + const savedObjectStore = new SavedObjectIndexStore(startDependencies.contentManagement); return startDependencies.embeddable.getAttributeService< LensSavedObjectAttributes, diff --git a/x-pack/plugins/lens/public/persistence/lens_client.ts b/x-pack/plugins/lens/public/persistence/lens_client.ts new file mode 100644 index 0000000000000..7517c26c87a10 --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/lens_client.ts @@ -0,0 +1,81 @@ +/* + * 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 { SearchQuery } from '@kbn/content-management-plugin/common'; +import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import type { + SerializableAttributes, + VisualizationClient, +} from '@kbn/visualizations-plugin/public'; +import { DOC_TYPE } from '../../common/constants'; +import { + LensCreateIn, + LensCreateOut, + LensDeleteIn, + LensDeleteOut, + LensGetIn, + LensGetOut, + LensSearchIn, + LensSearchOut, + LensSearchQuery, + LensUpdateIn, + LensUpdateOut, +} from '../../common/content_management'; + +export function getLensClient( + cm: ContentManagementPublicStart +): VisualizationClient<'lens', Attr> { + const get = async (id: string) => { + return cm.client.get({ + contentTypeId: DOC_TYPE, + id, + }); + }; + + const create = async ({ data, options }: Omit) => { + const res = await cm.client.create({ + contentTypeId: DOC_TYPE, + data, + options, + }); + return res; + }; + + const update = async ({ id, data, options }: Omit) => { + const res = await cm.client.update({ + contentTypeId: DOC_TYPE, + id, + data, + options, + }); + return res; + }; + + const deleteLens = async (id: string) => { + const res = await cm.client.delete({ + contentTypeId: DOC_TYPE, + id, + }); + return res; + }; + + const search = async (query: SearchQuery = {}, options?: LensSearchQuery) => { + return cm.client.search({ + contentTypeId: DOC_TYPE, + query, + options, + }); + }; + + return { + get, + create, + update, + delete: deleteLens, + search, + } as unknown as VisualizationClient<'lens', Attr>; +} diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts index dc0ec31178331..f7a6fa475a5ed 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ContentClient } from '@kbn/content-management-plugin/public'; +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import { SavedObjectIndexStore } from './saved_object_store'; describe('LensStore', () => { @@ -18,7 +18,10 @@ describe('LensStore', () => { return { client, - store: new SavedObjectIndexStore(client as unknown as ContentClient), + store: new SavedObjectIndexStore({ + client, + registry: jest.fn(), + } as unknown as ContentManagementPublicStart), }; } diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index 79ed252754126..d15386548dacf 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -7,21 +7,12 @@ import { Filter, Query } from '@kbn/es-query'; import { SavedObjectReference } from '@kbn/core/public'; -import { DataViewSpec } from '@kbn/data-views-plugin/public'; -import { ContentClient } from '@kbn/content-management-plugin/public'; -import { SearchQuery } from '@kbn/content-management-plugin/common'; -import { DOC_TYPE } from '../../common/constants'; -import { - LensCreateIn, - LensCreateOut, - LensGetIn, - LensGetOut, - LensSearchIn, - LensSearchOut, - LensSearchQuery, - LensUpdateIn, - LensUpdateOut, -} from '../../common/content_management'; +import type { DataViewSpec } from '@kbn/data-views-plugin/public'; +import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import type { SearchQuery } from '@kbn/content-management-plugin/common'; +import type { VisualizationClient } from '@kbn/visualizations-plugin/public'; +import type { LensSavedObjectAttributes, LensSearchQuery } from '../../common/content_management'; +import { getLensClient } from './lens_client'; export interface Document { savedObjectId?: string; @@ -55,10 +46,10 @@ export interface DocumentLoader { export type SavedObjectStore = DocumentLoader & DocumentSaver; export class SavedObjectIndexStore implements SavedObjectStore { - private client: ContentClient; + private client: VisualizationClient<'lens', LensSavedObjectAttributes>; - constructor(client: ContentClient) { - this.client = client; + constructor(cm: ContentManagementPublicStart) { + this.client = getLensClient(cm); } save = async (vis: Document) => { @@ -66,8 +57,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { const attributes = rest; if (savedObjectId) { - const result = await this.client.update({ - contentTypeId: 'lens', + const result = await this.client.update({ id: savedObjectId, data: attributes, options: { @@ -76,8 +66,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { }); return { ...vis, savedObjectId: result.item.id }; } else { - const result = await this.client.create({ - contentTypeId: 'lens', + const result = await this.client.create({ data: attributes, options: { references, @@ -88,10 +77,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { }; async load(savedObjectId: string) { - const resolveResult = await this.client.get({ - contentTypeId: DOC_TYPE, - id: savedObjectId, - }); + const resolveResult = await this.client.get(savedObjectId); if (resolveResult.item.error) { throw resolveResult.item.error; @@ -101,11 +87,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { } async search(query: SearchQuery, options: LensSearchQuery) { - const result = await this.client.search({ - contentTypeId: DOC_TYPE, - query, - options, - }); + const result = await this.client.search(query, options); return result; } } diff --git a/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts index 11a97ae82470f..2fc493df38edc 100644 --- a/x-pack/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { VisTypeAlias } from '@kbn/visualizations-plugin/public'; import { getBasePath, getEditPath } from '../common/constants'; +import { getLensClient } from './persistence/lens_client'; export const getLensAliasConfig = (): VisTypeAlias => ({ aliasPath: getBasePath(), @@ -30,6 +31,8 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ visualizations: { docTypes: ['lens'], searchFields: ['title^3'], + clientOptions: { update: { overwrite: true } }, + client: getLensClient, toListItem(savedObject) { const { id, type, updatedAt, attributes } = savedObject; const { title, description } = attributes as { title: string; description?: string }; diff --git a/x-pack/plugins/lens/server/content_management/lens_storage.ts b/x-pack/plugins/lens/server/content_management/lens_storage.ts index c394508a0d476..72f78472356e5 100644 --- a/x-pack/plugins/lens/server/content_management/lens_storage.ts +++ b/x-pack/plugins/lens/server/content_management/lens_storage.ts @@ -5,34 +5,35 @@ * 2.0. */ import Boom from '@hapi/boom'; -import type { SearchQuery } from '@kbn/content-management-plugin/common'; -import type { ContentStorage, StorageContext } from '@kbn/content-management-plugin/server'; -import type { - SavedObject, - SavedObjectReference, - SavedObjectsFindOptions, -} from '@kbn/core-saved-objects-api-server'; +import type { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server'; +import type { StorageContext } from '@kbn/content-management-plugin/server'; +import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils'; +import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server'; + +import { + CONTENT_ID, + type LensCrudTypes, + type LensSavedObject, + type LensSavedObjectAttributes, + type PartialLensSavedObject, +} from '../../common/content_management'; +import { cmServicesDefinition } from '../../common/content_management/cm_services'; -import { getMSearch, type GetMSearchType } from '@kbn/content-management-utils'; +const searchArgsToSOFindOptions = (args: LensCrudTypes['SearchIn']): SavedObjectsFindOptions => { + const { query, contentTypeId, options } = args; -import { CONTENT_ID } from '../../common/content_management'; -import { cmServicesDefinition } from '../../common/content_management/cm_services'; -import type { - LensSavedObjectAttributes, - LensSavedObject, - PartialLensSavedObject, - LensContentType, - LensGetOut, - LensCreateIn, - LensCreateOut, - CreateOptions, - LensUpdateIn, - LensUpdateOut, - UpdateOptions, - LensDeleteOut, - LensSearchQuery, - LensSearchOut, -} from '../../common/content_management'; + return { + type: contentTypeId, + searchFields: ['title^3', 'description'], + fields: ['description', 'title'], + search: query.text, + perPage: query.limit, + page: query.cursor ? +query.cursor : undefined, + defaultSearchOperator: 'AND', + ...options, + ...tagsToFindOptions(query.tags), + }; +}; const savedObjectClientFromRequest = async (ctx: StorageContext) => { if (!ctx.requestHandlerContext) { @@ -47,16 +48,6 @@ type PartialSavedObject = Omit>, 'references'> & { references: SavedObjectReference[] | undefined; }; -function savedObjectToLensSavedObject( - savedObject: SavedObject, - partial: false -): LensSavedObject; - -function savedObjectToLensSavedObject( - savedObject: PartialSavedObject, - partial: true -): PartialLensSavedObject; - function savedObjectToLensSavedObject( savedObject: | SavedObject @@ -90,117 +81,27 @@ function savedObjectToLensSavedObject( }; } -const SO_TYPE: LensContentType = 'lens'; - -export class LensStorage implements ContentStorage { - mSearch: GetMSearchType; +export class LensStorage extends SOContentStorage { constructor() { - this.mSearch = getMSearch({ - savedObjectType: SO_TYPE, + super({ + savedObjectType: CONTENT_ID, cmServicesDefinition, + searchArgsToSOFindOptions, + enableMSearch: true, allowedSavedObjectAttributes: ['title', 'description', 'visualizationType', 'state'], }); } - async get(ctx: StorageContext, id: string): Promise { - const { - utils: { getTransforms }, - version: { request: requestVersion }, - } = ctx; - const transforms = getTransforms(cmServicesDefinition, requestVersion); - const soClient = await savedObjectClientFromRequest(ctx); - - // Save data in DB - const { - saved_object: savedObject, - alias_purpose: aliasPurpose, - alias_target_id: aliasTargetId, - outcome, - } = await soClient.resolve(SO_TYPE, id); - - const response: LensGetOut = { - item: savedObjectToLensSavedObject(savedObject, false), - meta: { - aliasPurpose, - aliasTargetId, - outcome, - }, - }; - - // Validate DB response and DOWN transform to the request version - const { value, error: resultError } = transforms.get.out.result.down( - response - ); - - if (resultError) { - throw Boom.badRequest(`Invalid response. ${resultError.message}`); - } - - return value; - } - - async bulkGet(): Promise { - // Not implemented. Lens does not use bulkGet - throw new Error(`[bulkGet] has not been implemented. See LensStorage class.`); - } - - async create( - ctx: StorageContext, - data: LensCreateIn['data'], - options: CreateOptions - ): Promise { - const { - utils: { getTransforms }, - version: { request: requestVersion }, - } = ctx; - const transforms = getTransforms(cmServicesDefinition, requestVersion); - - // Validate input (data & options) & UP transform them to the latest version - const { value: dataToLatest, error: dataError } = transforms.create.in.data.up< - LensSavedObjectAttributes, - LensSavedObjectAttributes - >(data); - if (dataError) { - throw Boom.badRequest(`Invalid data. ${dataError.message}`); - } - - const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up< - CreateOptions, - CreateOptions - >(options); - if (optionsError) { - throw Boom.badRequest(`Invalid options. ${optionsError.message}`); - } - - // Save data in DB - const soClient = await savedObjectClientFromRequest(ctx); - const savedObject = await soClient.create( - SO_TYPE, - dataToLatest, - optionsToLatest - ); - - // Validate DB response and DOWN transform to the request version - const { value, error: resultError } = transforms.create.out.result.down< - LensCreateOut, - LensCreateOut - >({ - item: savedObjectToLensSavedObject(savedObject, false), - }); - - if (resultError) { - throw Boom.badRequest(`Invalid response. ${resultError.message}`); - } - - return value; - } - + /** + * Lens requires a custom update function because of https://github.com/elastic/kibana/issues/160116 + * where a forced create with overwrite flag is used instead of regular update + */ async update( ctx: StorageContext, id: string, - data: LensUpdateIn['data'], - options: UpdateOptions - ): Promise { + data: LensCrudTypes['UpdateIn']['data'], + options: LensCrudTypes['UpdateOptions'] + ): Promise { const { utils: { getTransforms }, version: { request: requestVersion }, @@ -217,8 +118,8 @@ export class LensStorage implements ContentStorage(options); if (optionsError) { throw Boom.badRequest(`Invalid options. ${optionsError.message}`); @@ -227,7 +128,7 @@ export class LensStorage implements ContentStorage(SO_TYPE, dataToLatest, { + const savedObject = await soClient.create(CONTENT_ID, dataToLatest, { id, overwrite: true, ...optionsToLatest, @@ -235,85 +136,10 @@ export class LensStorage implements ContentStorage({ - item: savedObjectToLensSavedObject(savedObject, true), - }); - - if (resultError) { - throw Boom.badRequest(`Invalid response. ${resultError.message}`); - } - - return value; - } - - async delete(ctx: StorageContext, id: string): Promise { - const soClient = await savedObjectClientFromRequest(ctx); - await soClient.delete(SO_TYPE, id); - return { success: true }; - } - - async search( - ctx: StorageContext, - query: SearchQuery, - options: LensSearchQuery = {} - ): Promise { - const { - utils: { getTransforms }, - version: { request: requestVersion }, - } = ctx; - const transforms = getTransforms(cmServicesDefinition, requestVersion); - const soClient = await savedObjectClientFromRequest(ctx); - - // Validate and UP transform the options - const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up< - LensSearchQuery, - LensSearchQuery - >(options); - if (optionsError) { - throw Boom.badRequest(`Invalid payload. ${optionsError.message}`); - } - const { searchFields = ['title^3', 'description'], types = [CONTENT_ID] } = optionsToLatest; - - const { included, excluded } = query.tags ?? {}; - const hasReference: SavedObjectsFindOptions['hasReference'] = included - ? included.map((id) => ({ - id, - type: 'tag', - })) - : undefined; - - const hasNoReference: SavedObjectsFindOptions['hasNoReference'] = excluded - ? excluded.map((id) => ({ - id, - type: 'tag', - })) - : undefined; - - const soQuery: SavedObjectsFindOptions = { - type: types, - search: query.text, - perPage: query.limit, - page: query.cursor ? Number(query.cursor) : undefined, - defaultSearchOperator: 'AND', - searchFields, - hasReference, - hasNoReference, - }; - - // Execute the query in the DB - const response = await soClient.find(soQuery); - - // Validate the response and DOWN transform to the request version - const { value, error: resultError } = transforms.search.out.result.down< - LensSearchOut, - LensSearchOut + LensCrudTypes['UpdateOut'], + LensCrudTypes['UpdateOut'] >({ - hits: response.saved_objects.map((so) => savedObjectToLensSavedObject(so, false)), - pagination: { - total: response.total, - }, + item: savedObjectToLensSavedObject(savedObject), }); if (resultError) { diff --git a/x-pack/plugins/maps/common/content_management/v1/cm_services.ts b/x-pack/plugins/maps/common/content_management/v1/cm_services.ts index ba849479776d5..4ef3a7b35fff8 100644 --- a/x-pack/plugins/maps/common/content_management/v1/cm_services.ts +++ b/x-pack/plugins/maps/common/content_management/v1/cm_services.ts @@ -67,7 +67,7 @@ export const serviceDefinition: ServicesDefinition = { update: { in: { options: { - schema: createOptionsSchema, // same schema as "create" + schema: createOptionsSchema, // same as create }, data: { schema: mapAttributesSchema, diff --git a/x-pack/plugins/maps/public/content_management/duplicate_title_check.ts b/x-pack/plugins/maps/public/content_management/duplicate_title_check.ts index eb8f4760774ce..560a8c034a208 100644 --- a/x-pack/plugins/maps/public/content_management/duplicate_title_check.ts +++ b/x-pack/plugins/maps/public/content_management/duplicate_title_check.ts @@ -8,7 +8,8 @@ import { i18n } from '@kbn/i18n'; import { OverlayStart } from '@kbn/core/public'; -import { mapsClient } from './maps_client'; +import type { MapAttributes } from '../../common/content_management'; +import { getMapClient } from './maps_client'; const rejectErrorMessage = i18n.translate('xpack.maps.saveDuplicateRejectedDescription', { defaultMessage: 'Save with duplicate title confirmation was rejected', @@ -48,7 +49,7 @@ export const checkForDuplicateTitle = async ( return true; } - const { hits } = await mapsClient.search( + const { hits } = await getMapClient().search( { text: `"${title}"`, limit: 10, diff --git a/x-pack/plugins/maps/public/content_management/index.ts b/x-pack/plugins/maps/public/content_management/index.ts index 8067e4250c9f1..c6894828067b5 100644 --- a/x-pack/plugins/maps/public/content_management/index.ts +++ b/x-pack/plugins/maps/public/content_management/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -export { mapsClient } from './maps_client'; +export { getMapClient } from './maps_client'; export { checkForDuplicateTitle } from './duplicate_title_check'; diff --git a/x-pack/plugins/maps/public/content_management/maps_client.ts b/x-pack/plugins/maps/public/content_management/maps_client.ts index 7cba8f69b96a9..98148102fbbc4 100644 --- a/x-pack/plugins/maps/public/content_management/maps_client.ts +++ b/x-pack/plugins/maps/public/content_management/maps_client.ts @@ -5,62 +5,65 @@ * 2.0. */ import type { SearchQuery } from '@kbn/content-management-plugin/common'; +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; +import type { + SerializableAttributes, + VisualizationClient, +} from '@kbn/visualizations-plugin/public/vis_types/vis_type_alias_registry'; import type { MapCrudTypes } from '../../common/content_management'; import { CONTENT_ID as contentTypeId } from '../../common/content_management'; import { getContentManagement } from '../kibana_services'; -const get = async (id: string) => { - return getContentManagement().client.get({ - contentTypeId, - id, - }); -}; +export function getMapClient( + cm: ContentManagementPublicStart = getContentManagement() +): VisualizationClient<'map', Attr> { + const get = async (id: string) => { + return cm.client.get({ + contentTypeId, + id, + }); + }; -const create = async ({ data, options }: Omit) => { - const res = await getContentManagement().client.create< - MapCrudTypes['CreateIn'], - MapCrudTypes['CreateOut'] - >({ - contentTypeId, - data, - options, - }); - return res; -}; + const create = async ({ data, options }: Omit) => { + const res = await cm.client.create({ + contentTypeId, + data, + options, + }); + return res; + }; -const update = async ({ id, data, options }: Omit) => { - const res = await getContentManagement().client.update< - MapCrudTypes['UpdateIn'], - MapCrudTypes['UpdateOut'] - >({ - contentTypeId, - id, - data, - options, - }); - return res; -}; + const update = async ({ id, data, options }: Omit) => { + const res = await cm.client.update({ + contentTypeId, + id, + data, + options, + }); + return res; + }; -const deleteMap = async (id: string) => { - await getContentManagement().client.delete({ - contentTypeId, - id, - }); -}; + const deleteMap = async (id: string) => { + return await cm.client.delete({ + contentTypeId, + id, + }); + }; -const search = async (query: SearchQuery = {}, options?: MapCrudTypes['SearchOptions']) => { - return getContentManagement().client.search({ - contentTypeId, - query, - options, - }); -}; + const search = async (query: SearchQuery = {}, options?: MapCrudTypes['SearchOptions']) => { + return cm.client.search({ + contentTypeId, + query, + options, + }); + }; -export const mapsClient = { - get, - create, - update, - delete: deleteMap, - search, -}; + return { + get, + create, + update, + delete: deleteMap, + search, + } as unknown as VisualizationClient<'map', Attr>; +} diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index 5a4d3e8cf0038..3fb1c8157c0f5 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -13,7 +13,7 @@ import type { MapAttributes } from '../common/content_management'; import { MAP_EMBEDDABLE_NAME, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { getCoreOverlays, getEmbeddableService } from './kibana_services'; import { extractReferences, injectReferences } from '../common/migrations/references'; -import { mapsClient, checkForDuplicateTitle } from './content_management'; +import { getMapClient, checkForDuplicateTitle } from './content_management'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; export interface SharingSavedObjectProps { @@ -65,8 +65,12 @@ export function getMapAttributeService(): MapAttributeService { const { item: { id }, } = await (savedObjectId - ? mapsClient.update({ id: savedObjectId, data: updatedAttributes, options: { references } }) - : mapsClient.create({ data: updatedAttributes, options: { references } })); + ? getMapClient().update({ + id: savedObjectId, + data: updatedAttributes, + options: { references }, + }) + : getMapClient().create({ data: updatedAttributes, options: { references } })); return { id }; }, unwrapMethod: async ( @@ -78,7 +82,7 @@ export function getMapAttributeService(): MapAttributeService { const { item: savedObject, meta: { outcome, aliasPurpose, aliasTargetId }, - } = await mapsClient.get(savedObjectId); + } = await getMapClient().get(savedObjectId); if (savedObject.error) { throw savedObject.error; diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.ts b/x-pack/plugins/maps/public/maps_vis_type_alias.ts index 548098311e3c2..bcbf0170afb25 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.ts +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.ts @@ -16,6 +16,7 @@ import { MAP_PATH, MAP_SAVED_OBJECT_TYPE, } from '../common/constants'; +import { getMapClient } from './content_management'; export function getMapsVisTypeAlias() { const appDescription = i18n.translate('xpack.maps.visTypeAlias.description', { @@ -34,6 +35,7 @@ export function getMapsVisTypeAlias() { visualizations: { docTypes: [MAP_SAVED_OBJECT_TYPE], searchFields: ['title^3'], + client: getMapClient, toListItem(mapItem: MapItem) { const { id, type, updatedAt, attributes } = mapItem; const { title, description } = attributes; diff --git a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx index f988c5b11ef98..42c76513e7529 100644 --- a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx @@ -11,7 +11,7 @@ import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import { ScopedHistory } from '@kbn/core/public'; import { MapsListView } from './maps_list_view'; import { APP_ID } from '../../../common/constants'; -import { mapsClient } from '../../content_management'; +import { getMapClient } from '../../content_management'; interface Props { history: ScopedHistory; @@ -26,7 +26,7 @@ export function LoadListAndRender(props: Props) { props.stateTransfer.clearEditorState(APP_ID); let ignore = false; - mapsClient + getMapClient() .search({ limit: 1 }) .then((results) => { if (!ignore) { diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx index ea2aefff97b6c..8c982a2853708 100644 --- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { TableListView } from '@kbn/content-management-table-list-view'; import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view'; -import type { MapItem } from '../../../common/content_management'; +import type { MapAttributes, MapItem } from '../../../common/content_management'; import { APP_ID, APP_NAME, getEditPath, MAP_PATH } from '../../../common/constants'; import { getMapsCapabilities, @@ -23,7 +23,7 @@ import { getUsageCollection, getServerless, } from '../../kibana_services'; -import { mapsClient } from '../../content_management'; +import { getMapClient } from '../../content_management'; const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit'; const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage'; @@ -55,7 +55,7 @@ const toTableListViewSavedObject = (mapItem: MapItem): MapUserContent => { }; async function deleteMaps(items: Array<{ id: string }>) { - await Promise.all(items.map(({ id }) => mapsClient.delete(id))); + await Promise.all(items.map(({ id }) => getMapClient().delete(id))); } interface Props { @@ -97,7 +97,7 @@ function MapsListViewComp({ history }: Props) { referencesToExclude?: SavedObjectsFindOptionsReference[]; } = {} ) => { - return mapsClient + return getMapClient() .search({ text: searchTerm ? `${searchTerm}*` : undefined, limit: getUiSettings().get(SAVED_OBJECTS_LIMIT_SETTING), diff --git a/x-pack/test/functional/apps/lens/group1/smokescreen.ts b/x-pack/test/functional/apps/lens/group1/smokescreen.ts index 4f167992a7e03..e33e65741bf66 100644 --- a/x-pack/test/functional/apps/lens/group1/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/group1/smokescreen.ts @@ -762,6 +762,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(hasVisualOptionsButton).to.be(false); }); + it('should allow edit meta-data for Lens chart on listing page', async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('Afancilenstest'); + await listingTable.inspectVisualization(); + await listingTable.editVisualizationDetails({ + title: 'Anewfancilenstest', + description: 'new description', + }); + await listingTable.searchForItemWithName('Anewfancilenstest'); + await listingTable.expectItemsCount('visualize', 1); + }); + it('should correctly optimize multiple percentile metrics', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); diff --git a/x-pack/test/functional/apps/maps/group4/visualize_create_menu.js b/x-pack/test/functional/apps/maps/group4/visualize_create_menu.js index f91bd55452fa6..d9b37b4aca240 100644 --- a/x-pack/test/functional/apps/maps/group4/visualize_create_menu.js +++ b/x-pack/test/functional/apps/maps/group4/visualize_create_menu.js @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['visualize', 'header', 'maps']); - + const listingTable = getService('listingTable'); const security = getService('security'); describe('visualize create menu', () => { @@ -85,5 +85,37 @@ export default function ({ getService, getPageObjects }) { expect(hasLegecyViz).to.equal(false); }); }); + describe('edit meta-data', () => { + before(async () => { + await security.testUser.setRoles( + ['global_maps_all', 'global_visualize_all', 'test_logstash_reader'], + { + skipBrowserRefresh: true, + } + ); + + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should allow to change meta-data on a map visualization', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickMapsApp(); + await PageObjects.maps.waitForLayersToLoad(); + await PageObjects.maps.saveMap('myTestMap'); + await PageObjects.visualize.gotoVisualizationLandingPage(); + await listingTable.searchForItemWithName('myTestMap'); + await listingTable.inspectVisualization(); + await listingTable.editVisualizationDetails({ + title: 'AnotherTestMap', + description: 'new description', + }); + await listingTable.searchForItemWithName('AnotherTestMap'); + await listingTable.expectItemsCount('visualize', 1); + }); + }); }); }