From 1432cce0f37838950915007bdfe64c871da51976 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 1 Sep 2023 16:45:09 +0200 Subject: [PATCH 01/16] :recycle: Refactor client to factory fn --- .../duplicate_title_check.ts | 4 +- .../maps/public/content_management/index.ts | 2 +- .../public/content_management/maps_client.ts | 95 +++++++++---------- .../maps/public/map_attribute_service.ts | 12 ++- .../maps/public/maps_vis_type_alias.ts | 2 + .../routes/list_page/load_list_and_render.tsx | 4 +- .../routes/list_page/maps_list_view.tsx | 6 +- 7 files changed, 64 insertions(+), 61 deletions(-) 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..a07fe79e30dca 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,7 @@ import { i18n } from '@kbn/i18n'; import { OverlayStart } from '@kbn/core/public'; -import { mapsClient } from './maps_client'; +import { getMapClient } from './maps_client'; const rejectErrorMessage = i18n.translate('xpack.maps.saveDuplicateRejectedDescription', { defaultMessage: 'Save with duplicate title confirmation was rejected', @@ -48,7 +48,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..195b9a2e9242c 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,59 @@ * 2.0. */ import type { SearchQuery } from '@kbn/content-management-plugin/common'; +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; 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()) { + 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) => { + 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, + }; +} diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index 5a4d3e8cf0038..77285e9c0a0d4 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..8787a798f8555 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 @@ -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), From 8663aef83b13c1bbcf4ccb5bd538138e1888dd2e Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 1 Sep 2023 16:45:38 +0200 Subject: [PATCH 02/16] :sparkles: Create lens client --- .../lens/public/lens_attribute_service.ts | 2 +- .../lens/public/persistence/lens_client.ts | 85 +++++++++++++++++++ .../public/persistence/saved_object_store.ts | 44 +++------- x-pack/plugins/lens/public/vis_type_alias.ts | 2 + 4 files changed, 101 insertions(+), 32 deletions(-) create mode 100644 x-pack/plugins/lens/public/persistence/lens_client.ts 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..2b7ea267609dc --- /dev/null +++ b/x-pack/plugins/lens/public/persistence/lens_client.ts @@ -0,0 +1,85 @@ +/* + * 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 { createGetterSetter } from '@kbn/kibana-utils-plugin/common'; +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 const [getContentManagement, setContentManagement] = + createGetterSetter('SavedObjectsManagement'); + +export function getLensClient( + cm: ContentManagementPublicStart = getContentManagement() +): 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.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..180631dda1b25 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,7 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ visualizations: { docTypes: ['lens'], searchFields: ['title^3'], + client: getLensClient, toListItem(savedObject) { const { id, type, updatedAt, attributes } = savedObject; const { title, description } = attributes as { title: string; description?: string }; From 5b4306727ba2859b42896cead5af8bdb59ab2b55 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 1 Sep 2023 16:46:07 +0200 Subject: [PATCH 03/16] :wrench: Add client prop to type service --- .../visualization_client.ts | 7 +++ src/plugins/visualizations/public/index.ts | 10 +++- .../update_basic_attributes.ts | 23 ++++++++- .../visualizations/public/vis_types/index.ts | 1 + .../vis_types/vis_type_alias_registry.ts | 47 +++++++++++++++++++ 5 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/plugins/visualizations/public/content_management/visualization_client.ts b/src/plugins/visualizations/public/content_management/visualization_client.ts index 38eec4141a045..1af073f466cd7 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) { + return getContentManagement().client.mSearch({ + contentTypes: options.types.map((type) => ({ contentTypeId: type })), + query, + options, + }); + } 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/utils/saved_objects_utils/update_basic_attributes.ts b/src/plugins/visualizations/public/utils/saved_objects_utils/update_basic_attributes.ts index ca6158422f58a..899dc9561b673 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,29 @@ 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 + ); } export const updateBasicSoAttributes = async ( @@ -27,7 +44,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,7 +67,7 @@ export const updateBasicSoAttributes = async ( ); } - return await visualizationsClient.update({ + return await client.update({ id: soId, data: { ...attributes, 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..832291eec83bf 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,49 @@ 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[]; + client: (contentManagement: ContentManagementPublicStart) => VisualizationClient; toListItem: (savedObject: SimpleSavedObject) => VisualizationListItem; } From 93b422b9282c7d387295912927e53c8d5b9c7cd4 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 1 Sep 2023 16:46:23 +0200 Subject: [PATCH 04/16] :white_check_mark: Add unit and functional tests --- .../utils/saved_visualize_utils.test.ts | 1 + .../dashboard/group4/dashboard_listing.ts | 12 +++---- .../visualize/group3/_visualize_listing.ts | 17 ++++++++++ .../apps/lens/group1/smokescreen.ts | 12 +++++++ .../apps/maps/group4/visualize_create_menu.js | 31 ++++++++++++++++++- 5 files changed, 64 insertions(+), 9 deletions(-) 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..78446fbff5f0c 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, }, })), })); 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/x-pack/test/functional/apps/lens/group1/smokescreen.ts b/x-pack/test/functional/apps/lens/group1/smokescreen.ts index dbd734348ba7d..324135ca05a6e 100644 --- a/x-pack/test/functional/apps/lens/group1/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/group1/smokescreen.ts @@ -761,5 +761,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const hasVisualOptionsButton = await PageObjects.lens.hasVisualOptionsButton(); 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); + }); }); } 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..cbcbe7b20868d 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,34 @@ export default function ({ getService, getPageObjects }) { expect(hasLegecyViz).to.equal(false); }); }); + describe('edit meta-data', () => { + before(async () => { + await security.testUser.setRoles(['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); + }); + }); }); } From d4c38d282c41d6fa31cf9bd343d8904e96e43789 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 1 Sep 2023 17:10:38 +0200 Subject: [PATCH 05/16] :bug: Fixes after manual testing --- src/plugins/content_management/common/rpc/msearch.ts | 6 ++++-- .../public/content_management/visualization_client.ts | 5 +++-- src/plugins/visualizations/public/plugin.ts | 1 + .../visualize_app/components/visualize_listing.tsx | 10 ++++++++-- .../visualizations/public/visualize_app/types.ts | 2 ++ x-pack/plugins/lens/public/persistence/lens_client.ts | 6 +----- .../lens/public/persistence/saved_object_store.test.ts | 4 ++-- .../maps/common/content_management/v1/cm_services.ts | 7 ++++++- .../maps/public/content_management/maps_client.ts | 2 +- 9 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/plugins/content_management/common/rpc/msearch.ts b/src/plugins/content_management/common/rpc/msearch.ts index 9ba1fc81c65bf..bdc49f44d4e1e 100644 --- a/src/plugins/content_management/common/rpc/msearch.ts +++ b/src/plugins/content_management/common/rpc/msearch.ts @@ -22,6 +22,7 @@ export const mSearchSchemas: ProcedureSchemas = { } ), query: searchQuerySchema, + options: schema.maybe(schema.object({}, { unknowns: 'allow' })), }, { unknowns: 'forbid' } ), @@ -45,7 +46,8 @@ export interface MSearchIn { export type MSearchResult = SearchResult; -export interface MSearchOut { +export interface MSearchIn { contentTypes: Array<{ contentTypeId: string; version?: Version }>; - result: MSearchResult; + query: MSearchQuery; + options?: Options; } diff --git a/src/plugins/visualizations/public/content_management/visualization_client.ts b/src/plugins/visualizations/public/content_management/visualization_client.ts index 1af073f466cd7..6169c02068989 100644 --- a/src/plugins/visualizations/public/content_management/visualization_client.ts +++ b/src/plugins/visualizations/public/content_management/visualization_client.ts @@ -67,10 +67,11 @@ const deleteVisualization = async (id: string) => { const search = async (query: SearchQuery = {}, options?: VisualizationSearchQuery) => { if (options && options.types && options.types.length > 1) { + const { types, ...rest } = options; return getContentManagement().client.mSearch({ - contentTypes: options.types.map((type) => ({ contentTypeId: type })), + contentTypes: types.map((type) => ({ contentTypeId: type })), query, - options, + options: rest, }); } return getContentManagement().client.search({ 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/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/x-pack/plugins/lens/public/persistence/lens_client.ts b/x-pack/plugins/lens/public/persistence/lens_client.ts index 2b7ea267609dc..7517c26c87a10 100644 --- a/x-pack/plugins/lens/public/persistence/lens_client.ts +++ b/x-pack/plugins/lens/public/persistence/lens_client.ts @@ -7,7 +7,6 @@ import type { SearchQuery } from '@kbn/content-management-plugin/common'; import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; -import { createGetterSetter } from '@kbn/kibana-utils-plugin/common'; import type { SerializableAttributes, VisualizationClient, @@ -27,11 +26,8 @@ import { LensUpdateOut, } from '../../common/content_management'; -export const [getContentManagement, setContentManagement] = - createGetterSetter('SavedObjectsManagement'); - export function getLensClient( - cm: ContentManagementPublicStart = getContentManagement() + cm: ContentManagementPublicStart ): VisualizationClient<'lens', Attr> { const get = async (id: string) => { return cm.client.get({ 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..c0f0c84d813cd 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,7 @@ describe('LensStore', () => { return { client, - store: new SavedObjectIndexStore(client as unknown as ContentClient), + store: new SavedObjectIndexStore(client as unknown as ContentManagementPublicStart), }; } 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..e43c93203ba30 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 @@ -39,6 +39,11 @@ const createOptionsSchema = schema.object({ references: schema.maybe(createOptionsSchemas.references), }); +const updateOptionsSchema = schema.object({ + overwrite: schema.maybe(createOptionsSchemas.overwrite), + references: schema.maybe(createOptionsSchemas.references), +}); + // Content management service definition. // We need it for BWC support between different versions of the content export const serviceDefinition: ServicesDefinition = { @@ -67,7 +72,7 @@ export const serviceDefinition: ServicesDefinition = { update: { in: { options: { - schema: createOptionsSchema, // same schema as "create" + schema: updateOptionsSchema, }, data: { schema: mapAttributesSchema, 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 195b9a2e9242c..d278ce7cdedb2 100644 --- a/x-pack/plugins/maps/public/content_management/maps_client.ts +++ b/x-pack/plugins/maps/public/content_management/maps_client.ts @@ -39,7 +39,7 @@ export function getMapClient(cm: ContentManagementPublicStart = getContentManage }; const deleteMap = async (id: string) => { - await cm.client.delete({ + return await cm.client.delete({ contentTypeId, id, }); From 7dc072bc69c596831c1fb91f54deac28a0da22e0 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 1 Sep 2023 17:16:01 +0200 Subject: [PATCH 06/16] :sparkles: Add new methods to better interact with listing page --- test/functional/services/listing_table.ts | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) 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 */ From 950750158d124ae9bcb61c216036ae26855a0094 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 1 Sep 2023 17:37:25 +0200 Subject: [PATCH 07/16] :label: Fix type --- src/plugins/content_management/common/rpc/msearch.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/plugins/content_management/common/rpc/msearch.ts b/src/plugins/content_management/common/rpc/msearch.ts index bdc49f44d4e1e..4510d6c64c4ba 100644 --- a/src/plugins/content_management/common/rpc/msearch.ts +++ b/src/plugins/content_management/common/rpc/msearch.ts @@ -51,3 +51,8 @@ export interface MSearchIn { query: MSearchQuery; options?: Options; } + +export interface MSearchOut { + contentTypes: Array<{ contentTypeId: string; version?: Version }>; + result: MSearchResult; +} From 9bb387bb98f509c026af6f5f761596ca9862f073 Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 1 Sep 2023 18:27:59 +0200 Subject: [PATCH 08/16] :label: More type fixes --- packages/kbn-content-management-utils/src/types.ts | 2 ++ .../saved_visualization_references.ts | 4 ++-- x-pack/plugins/lens/public/app_plugin/mounter.tsx | 2 +- .../public/content_management/duplicate_title_check.ts | 3 ++- .../maps/public/content_management/maps_client.ts | 10 ++++++++-- x-pack/plugins/maps/public/map_attribute_service.ts | 2 +- .../maps/public/routes/list_page/maps_list_view.tsx | 4 ++-- 7 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/kbn-content-management-utils/src/types.ts b/packages/kbn-content-management-utils/src/types.ts index 7dce7961290af..4f8509872d1fd 100644 --- a/packages/kbn-content-management-utils/src/types.ts +++ b/packages/kbn-content-management-utils/src/types.ts @@ -161,6 +161,8 @@ export interface SavedObjectSearchOptions { /** Saved Object update options - Pick and Omit to customize */ export interface SavedObjectUpdateOptions { + /** Overwrite existing documents (defaults to false) */ + overwrite?: boolean; /** Array of referenced saved objects. */ references?: Reference[]; version?: string; 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/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/maps/public/content_management/duplicate_title_check.ts b/x-pack/plugins/maps/public/content_management/duplicate_title_check.ts index a07fe79e30dca..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,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { OverlayStart } from '@kbn/core/public'; +import type { MapAttributes } from '../../common/content_management'; import { getMapClient } from './maps_client'; const rejectErrorMessage = i18n.translate('xpack.maps.saveDuplicateRejectedDescription', { @@ -48,7 +49,7 @@ export const checkForDuplicateTitle = async ( return true; } - const { hits } = await getMapClient().search( + const { hits } = await getMapClient().search( { text: `"${title}"`, limit: 10, 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 d278ce7cdedb2..98148102fbbc4 100644 --- a/x-pack/plugins/maps/public/content_management/maps_client.ts +++ b/x-pack/plugins/maps/public/content_management/maps_client.ts @@ -6,12 +6,18 @@ */ 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'; -export function getMapClient(cm: ContentManagementPublicStart = getContentManagement()) { +export function getMapClient( + cm: ContentManagementPublicStart = getContentManagement() +): VisualizationClient<'map', Attr> { const get = async (id: string) => { return cm.client.get({ contentTypeId, @@ -59,5 +65,5 @@ export function getMapClient(cm: ContentManagementPublicStart = getContentManage 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 77285e9c0a0d4..3fb1c8157c0f5 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -82,7 +82,7 @@ export function getMapAttributeService(): MapAttributeService { const { item: savedObject, meta: { outcome, aliasPurpose, aliasTargetId }, - } = await getMapClient().get(savedObjectId); + } = await getMapClient().get(savedObjectId); if (savedObject.error) { throw savedObject.error; 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 8787a798f8555..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, @@ -97,7 +97,7 @@ function MapsListViewComp({ history }: Props) { referencesToExclude?: SavedObjectsFindOptionsReference[]; } = {} ) => { - return getMapClient() + return getMapClient() .search({ text: searchTerm ? `${searchTerm}*` : undefined, limit: getUiSettings().get(SAVED_OBJECTS_LIMIT_SETTING), From f337a34b2ac712e1b7630350e0c9d5d843ea44fc Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 1 Sep 2023 18:48:33 +0200 Subject: [PATCH 09/16] :white_check_mark: Fix tests --- .../public/utils/saved_visualize_utils.test.ts | 13 +++++++++++-- .../public/persistence/saved_object_store.test.ts | 5 ++++- 2 files changed, 15 insertions(+), 3 deletions(-) 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 78446fbff5f0c..5ff8fc2f22845 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts @@ -359,8 +359,12 @@ describe('saved_visualize_utils', () => { expect(mockFindContent.mock.calls).toMatchObject([ [ { + contentTypes: [ + { contentTypeId: 'bazdoc' }, + { contentTypeId: 'etc' }, + { contentTypeId: 'visualization' }, + ], options: { - types: ['bazdoc', 'etc', 'visualization'], searchFields: ['baz', 'bing', 'title^3', 'description'], }, }, @@ -396,8 +400,13 @@ describe('saved_visualize_utils', () => { expect(mockFindContent.mock.calls).toMatchObject([ [ { + contentTypes: [ + { contentTypeId: 'bazdoc' }, + { contentTypeId: 'bar' }, + { contentTypeId: 'visualization' }, + { contentTypeId: 'foo' }, + ], options: { - types: ['bazdoc', 'bar', 'visualization', 'foo'], searchFields: ['baz', 'bing', 'barfield', 'foofield', 'title^3', 'description'], }, }, 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 c0f0c84d813cd..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 @@ -18,7 +18,10 @@ describe('LensStore', () => { return { client, - store: new SavedObjectIndexStore(client as unknown as ContentManagementPublicStart), + store: new SavedObjectIndexStore({ + client, + registry: jest.fn(), + } as unknown as ContentManagementPublicStart), }; } From 42022691c303c2f8f005f94b40169d320e070cbc Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 4 Sep 2023 09:58:50 +0200 Subject: [PATCH 10/16] :recycle: Refactor types and storage --- .../lens/common/content_management/index.ts | 1 + .../common/content_management/v1/index.ts | 2 +- .../common/content_management/v1/types.ts | 114 +++--- .../server/content_management/lens_storage.ts | 326 ++---------------- 4 files changed, 61 insertions(+), 382 deletions(-) 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..9a6da0d43e4ac 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,9 @@ * 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 { ContentManagementCrudTypes } from '@kbn/content-management-utils'; + +import type { LensContentType } from '../types'; export interface Reference { type: string; @@ -26,6 +15,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 +39,37 @@ 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; - }; -} +export type LensCrudTypes = ContentManagementCrudTypes< + LensContentType, + LensSavedObjectAttributes, + CreateOptions, + UpdateOptions, + LensSearchQuery +>; + +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/server/content_management/lens_storage.ts b/x-pack/plugins/lens/server/content_management/lens_storage.ts index c394508a0d476..356d35543c566 100644 --- a/x-pack/plugins/lens/server/content_management/lens_storage.ts +++ b/x-pack/plugins/lens/server/content_management/lens_storage.ts @@ -4,322 +4,36 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 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 { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils'; -import { getMSearch, type GetMSearchType } from '@kbn/content-management-utils'; - -import { CONTENT_ID } from '../../common/content_management'; +import { CONTENT_ID, LensCrudTypes } 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'; - -const savedObjectClientFromRequest = async (ctx: StorageContext) => { - if (!ctx.requestHandlerContext) { - throw new Error('Storage context.requestHandlerContext missing.'); - } - - const { savedObjects } = await ctx.requestHandlerContext.core; - return savedObjects.client; -}; - -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 - | PartialSavedObject -): LensSavedObject | PartialLensSavedObject { - const { - id, - type, - updated_at: updatedAt, - created_at: createdAt, - attributes: { title, description, state, visualizationType }, - references, - error, - namespaces, - } = savedObject; +const searchArgsToSOFindOptions = (args: LensCrudTypes['SearchIn']): SavedObjectsFindOptions => { + const { query, contentTypeId, options } = args; return { - id, - type, - updatedAt, - createdAt, - attributes: { - title, - description, - visualizationType, - state, - }, - references, - error, - namespaces, + 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 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; - } - - async update( - ctx: StorageContext, - id: string, - data: LensUpdateIn['data'], - options: UpdateOptions - ): 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.update.in.data.up< - LensSavedObjectAttributes, - LensSavedObjectAttributes - >(data); - if (dataError) { - throw Boom.badRequest(`Invalid data. ${dataError.message}`); - } - - const { value: optionsToLatest, error: optionsError } = transforms.update.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, { - id, - overwrite: true, - ...optionsToLatest, - }); - - // Validate DB response and DOWN transform to the request version - const { value, error: resultError } = transforms.update.out.result.down< - LensUpdateOut, - LensUpdateOut - >({ - 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 - >({ - hits: response.saved_objects.map((so) => savedObjectToLensSavedObject(so, false)), - pagination: { - total: response.total, - }, - }); - - if (resultError) { - throw Boom.badRequest(`Invalid response. ${resultError.message}`); - } - - return value; - } } From b52dc99fc68b876a052d9756f688b7c3748bb45f Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 4 Sep 2023 09:59:04 +0200 Subject: [PATCH 11/16] :white_check_mark: Fix test --- .../functional/apps/maps/group4/visualize_create_menu.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 cbcbe7b20868d..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 @@ -87,9 +87,12 @@ export default function ({ getService, getPageObjects }) { }); describe('edit meta-data', () => { before(async () => { - await security.testUser.setRoles(['global_visualize_all', 'test_logstash_reader'], { - skipBrowserRefresh: true, - }); + await security.testUser.setRoles( + ['global_maps_all', 'global_visualize_all', 'test_logstash_reader'], + { + skipBrowserRefresh: true, + } + ); await PageObjects.visualize.navigateToNewAggBasedVisualization(); }); From 1fb3457c6245685d5b36e3bab2d89ede8073d08b Mon Sep 17 00:00:00 2001 From: dej611 Date: Mon, 4 Sep 2023 17:20:57 +0200 Subject: [PATCH 12/16] :recycle: Remove unused options --- src/plugins/content_management/common/rpc/msearch.ts | 6 ------ .../public/content_management/visualization_client.ts | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/plugins/content_management/common/rpc/msearch.ts b/src/plugins/content_management/common/rpc/msearch.ts index 4510d6c64c4ba..5e03bd2999b5a 100644 --- a/src/plugins/content_management/common/rpc/msearch.ts +++ b/src/plugins/content_management/common/rpc/msearch.ts @@ -46,12 +46,6 @@ export interface MSearchIn { export type MSearchResult = SearchResult; -export interface MSearchIn { - contentTypes: Array<{ contentTypeId: string; version?: Version }>; - query: MSearchQuery; - options?: Options; -} - export interface MSearchOut { contentTypes: Array<{ contentTypeId: string; version?: Version }>; result: MSearchResult; diff --git a/src/plugins/visualizations/public/content_management/visualization_client.ts b/src/plugins/visualizations/public/content_management/visualization_client.ts index 6169c02068989..957c9db39a9c4 100644 --- a/src/plugins/visualizations/public/content_management/visualization_client.ts +++ b/src/plugins/visualizations/public/content_management/visualization_client.ts @@ -67,11 +67,10 @@ const deleteVisualization = async (id: string) => { const search = async (query: SearchQuery = {}, options?: VisualizationSearchQuery) => { if (options && options.types && options.types.length > 1) { - const { types, ...rest } = options; + const { types } = options; return getContentManagement().client.mSearch({ contentTypes: types.map((type) => ({ contentTypeId: type })), query, - options: rest, }); } return getContentManagement().client.search({ From e2d9b9251e4e5ba2518faab9b6eab1728d1de180 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 5 Sep 2023 09:24:36 +0200 Subject: [PATCH 13/16] Update src/plugins/content_management/common/rpc/msearch.ts --- src/plugins/content_management/common/rpc/msearch.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/content_management/common/rpc/msearch.ts b/src/plugins/content_management/common/rpc/msearch.ts index 5e03bd2999b5a..9ba1fc81c65bf 100644 --- a/src/plugins/content_management/common/rpc/msearch.ts +++ b/src/plugins/content_management/common/rpc/msearch.ts @@ -22,7 +22,6 @@ export const mSearchSchemas: ProcedureSchemas = { } ), query: searchQuerySchema, - options: schema.maybe(schema.object({}, { unknowns: 'allow' })), }, { unknowns: 'forbid' } ), From 3bd5cc01e9fd60d8ec20bc62135c180280f49260 Mon Sep 17 00:00:00 2001 From: dej611 Date: Tue, 5 Sep 2023 09:27:34 +0200 Subject: [PATCH 14/16] :white_check_mark: Fix tests --- .../public/utils/saved_visualize_utils.test.ts | 6 ------ 1 file changed, 6 deletions(-) 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 5ff8fc2f22845..01475fe483c80 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts @@ -364,9 +364,6 @@ describe('saved_visualize_utils', () => { { contentTypeId: 'etc' }, { contentTypeId: 'visualization' }, ], - options: { - searchFields: ['baz', 'bing', 'title^3', 'description'], - }, }, ], ]); @@ -406,9 +403,6 @@ describe('saved_visualize_utils', () => { { contentTypeId: 'visualization' }, { contentTypeId: 'foo' }, ], - options: { - searchFields: ['baz', 'bing', 'barfield', 'foofield', 'title^3', 'description'], - }, }, ], ]); From d9b042756e81609e0f8c484877fba8ad06d6f90a Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 8 Sep 2023 12:25:57 +0200 Subject: [PATCH 15/16] :ok_hand: Refactor based on feedback --- .../kbn-content-management-utils/src/types.ts | 2 - .../vis_types/vis_type_alias_registry.ts | 2 +- .../common/content_management/v1/types.ts | 19 +-- .../server/content_management/lens_storage.ts | 114 +++++++++++++++++- .../content_management/v1/cm_services.ts | 7 +- 5 files changed, 127 insertions(+), 17 deletions(-) diff --git a/packages/kbn-content-management-utils/src/types.ts b/packages/kbn-content-management-utils/src/types.ts index 4f8509872d1fd..7dce7961290af 100644 --- a/packages/kbn-content-management-utils/src/types.ts +++ b/packages/kbn-content-management-utils/src/types.ts @@ -161,8 +161,6 @@ export interface SavedObjectSearchOptions { /** Saved Object update options - Pick and Omit to customize */ export interface SavedObjectUpdateOptions { - /** Overwrite existing documents (defaults to false) */ - overwrite?: boolean; /** Array of referenced saved objects. */ references?: Reference[]; version?: string; 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 832291eec83bf..1851fc129000a 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 @@ -45,7 +45,7 @@ export type GenericVisualizationCrudTypes< ContentType, Attr, Pick, - Pick, + Pick, object >; 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 9a6da0d43e4ac..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,6 +5,7 @@ * 2.0. */ +import type { UpdateIn } from '@kbn/content-management-plugin/common'; import type { ContentManagementCrudTypes } from '@kbn/content-management-utils'; import type { LensContentType } from '../types'; @@ -39,13 +40,17 @@ export type LensSavedObjectAttributes = { state?: unknown; }; -export type LensCrudTypes = ContentManagementCrudTypes< - LensContentType, - LensSavedObjectAttributes, - CreateOptions, - UpdateOptions, - LensSearchQuery ->; +// 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']; 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 356d35543c566..72f78472356e5 100644 --- a/x-pack/plugins/lens/server/content_management/lens_storage.ts +++ b/x-pack/plugins/lens/server/content_management/lens_storage.ts @@ -4,10 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import Boom from '@hapi/boom'; 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, LensCrudTypes } from '../../common/content_management'; +import { + CONTENT_ID, + type LensCrudTypes, + type LensSavedObject, + type LensSavedObjectAttributes, + type PartialLensSavedObject, +} from '../../common/content_management'; import { cmServicesDefinition } from '../../common/content_management/cm_services'; const searchArgsToSOFindOptions = (args: LensCrudTypes['SearchIn']): SavedObjectsFindOptions => { @@ -26,6 +35,52 @@ const searchArgsToSOFindOptions = (args: LensCrudTypes['SearchIn']): SavedObject }; }; +const savedObjectClientFromRequest = async (ctx: StorageContext) => { + if (!ctx.requestHandlerContext) { + throw new Error('Storage context.requestHandlerContext missing.'); + } + + const { savedObjects } = await ctx.requestHandlerContext.core; + return savedObjects.client; +}; + +type PartialSavedObject = Omit>, 'references'> & { + references: SavedObjectReference[] | undefined; +}; + +function savedObjectToLensSavedObject( + savedObject: + | SavedObject + | PartialSavedObject +): LensSavedObject | PartialLensSavedObject { + const { + id, + type, + updated_at: updatedAt, + created_at: createdAt, + attributes: { title, description, state, visualizationType }, + references, + error, + namespaces, + } = savedObject; + + return { + id, + type, + updatedAt, + createdAt, + attributes: { + title, + description, + visualizationType, + state, + }, + references, + error, + namespaces, + }; +} + export class LensStorage extends SOContentStorage { constructor() { super({ @@ -36,4 +91,61 @@ export class LensStorage extends SOContentStorage { allowedSavedObjectAttributes: ['title', 'description', 'visualizationType', 'state'], }); } + + /** + * 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: LensCrudTypes['UpdateIn']['data'], + options: LensCrudTypes['UpdateOptions'] + ): 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.update.in.data.up< + LensSavedObjectAttributes, + LensSavedObjectAttributes + >(data); + if (dataError) { + throw Boom.badRequest(`Invalid data. ${dataError.message}`); + } + + const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up< + LensCrudTypes['CreateOptions'], + LensCrudTypes['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(CONTENT_ID, dataToLatest, { + id, + overwrite: true, + ...optionsToLatest, + }); + + // Validate DB response and DOWN transform to the request version + const { value, error: resultError } = transforms.update.out.result.down< + LensCrudTypes['UpdateOut'], + LensCrudTypes['UpdateOut'] + >({ + item: savedObjectToLensSavedObject(savedObject), + }); + + if (resultError) { + throw Boom.badRequest(`Invalid response. ${resultError.message}`); + } + + return value; + } } 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 e43c93203ba30..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 @@ -39,11 +39,6 @@ const createOptionsSchema = schema.object({ references: schema.maybe(createOptionsSchemas.references), }); -const updateOptionsSchema = schema.object({ - overwrite: schema.maybe(createOptionsSchemas.overwrite), - references: schema.maybe(createOptionsSchemas.references), -}); - // Content management service definition. // We need it for BWC support between different versions of the content export const serviceDefinition: ServicesDefinition = { @@ -72,7 +67,7 @@ export const serviceDefinition: ServicesDefinition = { update: { in: { options: { - schema: updateOptionsSchema, + schema: createOptionsSchema, // same as create }, data: { schema: mapAttributesSchema, From 6c1c096bce528e619a5f2951764e7faa5f511eef Mon Sep 17 00:00:00 2001 From: dej611 Date: Fri, 8 Sep 2023 15:52:33 +0200 Subject: [PATCH 16/16] :bug: Make update overwrite an optional prop owned by each plugin --- .../update_basic_attributes.ts | 15 ++++++++++++++- .../public/vis_types/vis_type_alias_registry.ts | 5 +++++ x-pack/plugins/lens/public/vis_type_alias.ts | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) 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 899dc9561b673..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 @@ -34,6 +34,19 @@ function getClientForType( ); } +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 ( soId: string, type: string, @@ -73,8 +86,8 @@ export const updateBasicSoAttributes = async ( ...attributes, }, options: { - overwrite: true, references, + ...getAdditionalOptionsForUpdate(type, dependencies.typesService, 'update'), }, }); }; 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 1851fc129000a..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 @@ -76,6 +76,11 @@ export interface VisualizationClient< 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/x-pack/plugins/lens/public/vis_type_alias.ts b/x-pack/plugins/lens/public/vis_type_alias.ts index 180631dda1b25..2fc493df38edc 100644 --- a/x-pack/plugins/lens/public/vis_type_alias.ts +++ b/x-pack/plugins/lens/public/vis_type_alias.ts @@ -31,6 +31,7 @@ export const getLensAliasConfig = (): VisTypeAlias => ({ visualizations: { docTypes: ['lens'], searchFields: ['title^3'], + clientOptions: { update: { overwrite: true } }, client: getLensClient, toListItem(savedObject) { const { id, type, updatedAt, attributes } = savedObject;