diff --git a/packages/kbn-object-versioning/lib/content_management_types.ts b/packages/kbn-object-versioning/lib/content_management_types.ts index f6304d14752c7..56d72013335cd 100644 --- a/packages/kbn-object-versioning/lib/content_management_types.ts +++ b/packages/kbn-object-versioning/lib/content_management_types.ts @@ -64,52 +64,52 @@ export interface ServicesDefinition { export interface ServiceTransforms { get: { in: { - options: ObjectTransforms; + options: ObjectTransforms; }; out: { - result: ObjectTransforms; + result: ObjectTransforms; }; }; bulkGet: { in: { - options: ObjectTransforms; + options: ObjectTransforms; }; out: { - result: ObjectTransforms; + result: ObjectTransforms; }; }; create: { in: { - data: ObjectTransforms; - options: ObjectTransforms; + data: ObjectTransforms; + options: ObjectTransforms; }; out: { - result: ObjectTransforms; + result: ObjectTransforms; }; }; update: { in: { - data: ObjectTransforms; - options: ObjectTransforms; + data: ObjectTransforms; + options: ObjectTransforms; }; out: { - result: ObjectTransforms; + result: ObjectTransforms; }; }; delete: { in: { - options: ObjectTransforms; + options: ObjectTransforms; }; out: { - result: ObjectTransforms; + result: ObjectTransforms; }; }; search: { in: { - options: ObjectTransforms; + options: ObjectTransforms; }; out: { - result: ObjectTransforms; + result: ObjectTransforms; }; }; } diff --git a/packages/kbn-object-versioning/lib/object_transform.test.ts b/packages/kbn-object-versioning/lib/object_transform.test.ts index 795b9582c5c1b..4e7d451272d6d 100644 --- a/packages/kbn-object-versioning/lib/object_transform.test.ts +++ b/packages/kbn-object-versioning/lib/object_transform.test.ts @@ -9,12 +9,7 @@ import { schema } from '@kbn/config-schema'; import { initTransform } from './object_transform'; -import type { - ObjectMigrationDefinition, - ObjectTransforms, - Version, - VersionableObject, -} from './types'; +import type { ObjectMigrationDefinition, Version, VersionableObject } from './types'; interface FooV1 { fullName: string; @@ -58,7 +53,7 @@ const fooMigrationDef: ObjectMigrationDefinition = { const setup = ( browserVersion: Version -): ObjectTransforms => { +) => { const transformsFactory = initTransform(browserVersion); return transformsFactory(fooMigrationDef); }; diff --git a/packages/kbn-object-versioning/lib/object_transform.ts b/packages/kbn-object-versioning/lib/object_transform.ts index 9cc3e32e41366..dc34301529f6f 100644 --- a/packages/kbn-object-versioning/lib/object_transform.ts +++ b/packages/kbn-object-versioning/lib/object_transform.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ObjectMigrationDefinition, ObjectTransform, ObjectTransforms, Version } from './types'; +import { ObjectMigrationDefinition, ObjectTransform, Version } from './types'; import { validateObj, validateVersion } from './utils'; /** @@ -97,11 +97,8 @@ const getTransformFns = ( */ export const initTransform = (requestVersion: Version) => - ( - migrationDefinition: ObjectMigrationDefinition - ): ObjectTransforms => { + (migrationDefinition: ObjectMigrationDefinition) => { const { latestVersion } = getVersionsMeta(migrationDefinition); - const getVersion = (v: Version | 'latest'): Version => (v === 'latest' ? latestVersion : v); const validateFn = (value: unknown, version: number = requestVersion) => { @@ -120,7 +117,11 @@ export const initTransform = }; return { - up: (obj, to = 'latest', { validate = true }: { validate?: boolean } = {}) => { + up: ( + obj: I, + to: number | 'latest' = 'latest', + { validate = true }: { validate?: boolean } = {} + ) => { try { if (!migrationDefinition[requestVersion]) { return { @@ -145,16 +146,12 @@ export const initTransform = }; } - const fns = getTransformFns( - requestVersion, - targetVersion, - migrationDefinition - ); + const fns = getTransformFns(requestVersion, targetVersion, migrationDefinition); const value = fns.reduce((acc, fn) => { - const res = fn(acc as unknown as UpIn); + const res = fn(acc as unknown as I); return res; - }, obj as unknown as UpOut); + }, obj as unknown as O); return { value, error: null }; } catch (e) { @@ -164,7 +161,11 @@ export const initTransform = }; } }, - down: (obj, from = 'latest', { validate = true }: { validate?: boolean } = {}) => { + down: ( + obj: I, + from: number | 'latest' = 'latest', + { validate = true }: { validate?: boolean } = {} + ) => { try { if (!migrationDefinition[requestVersion]) { return { @@ -189,16 +190,12 @@ export const initTransform = } } - const fns = getTransformFns( - fromVersion, - requestVersion, - migrationDefinition - ); + const fns = getTransformFns(fromVersion, requestVersion, migrationDefinition); const value = fns.reduce((acc, fn) => { - const res = fn(acc as unknown as DownIn); + const res = fn(acc as unknown as I); return res; - }, obj as unknown as DownOut); + }, obj as unknown as O); return { value: value as any, error: null }; } catch (e) { diff --git a/packages/kbn-object-versioning/lib/types.ts b/packages/kbn-object-versioning/lib/types.ts index 7bf3e8aefac7c..bde5d534a723f 100644 --- a/packages/kbn-object-versioning/lib/types.ts +++ b/packages/kbn-object-versioning/lib/types.ts @@ -42,21 +42,21 @@ export interface ObjectTransforms< DownIn = unknown, DownOut = unknown > { - up: ( - obj: UpIn, + up: ( + obj: I, version?: Version | 'latest', options?: { /** Validate the object _before_ up transform */ validate?: boolean; } - ) => TransformReturn; - down: ( + ) => TransformReturn; + down: ( obj: DownIn, version?: Version | 'latest', options?: { /** Validate the object _before_ down transform */ validate?: boolean; } - ) => TransformReturn; + ) => TransformReturn; validate: (obj: any, version?: Version) => ValidationError | null; } diff --git a/src/plugins/content_management/public/rpc_client/rpc_client.ts b/src/plugins/content_management/public/rpc_client/rpc_client.ts index 17ea6a1391a59..08aa3f500cccc 100644 --- a/src/plugins/content_management/public/rpc_client/rpc_client.ts +++ b/src/plugins/content_management/public/rpc_client/rpc_client.ts @@ -34,11 +34,11 @@ export class RpcClient implements CrudClient { constructor(private http: { post: HttpSetup['post'] }) {} public get(input: I) { - return this.sendMessage>('get', input).then((r) => r.item); + return this.sendMessage>('get', input).then((r) => r.result); } public bulkGet(input: I) { - return this.sendMessage>('bulkGet', input).then((r) => r.items); + return this.sendMessage>('bulkGet', input).then((r) => r.result); } public create(input: I) { diff --git a/src/plugins/content_management/server/core/core.test.ts b/src/plugins/content_management/server/core/core.test.ts index 86e3797d2d92b..15671b42f4159 100644 --- a/src/plugins/content_management/server/core/core.test.ts +++ b/src/plugins/content_management/server/core/core.test.ts @@ -165,7 +165,7 @@ describe('Content Core', () => { const { fooContentCrud, ctx, cleanUp } = setup({ registerFooType: true }); const res = await fooContentCrud!.get(ctx, '1'); - expect(res.item.item).toBeUndefined(); + expect(res.result.item).toBeUndefined(); cleanUp(); }); @@ -176,7 +176,7 @@ describe('Content Core', () => { const res = await fooContentCrud!.get(ctx, '1', { forwardInResponse: { foo: 'bar' } }); expect(res).toEqual({ contentTypeId: FOO_CONTENT_ID, - item: { + result: { item: { // Options forwared in response options: { foo: 'bar' }, @@ -191,7 +191,7 @@ describe('Content Core', () => { const { fooContentCrud, ctx, cleanUp } = setup({ registerFooType: true }); const res = await fooContentCrud!.bulkGet(ctx, ['1', '2']); - expect(res.items).toEqual({ + expect(res.result).toEqual({ hits: [{ item: undefined }, { item: undefined }], }); @@ -207,7 +207,7 @@ describe('Content Core', () => { expect(res).toEqual({ contentTypeId: FOO_CONTENT_ID, - items: { + result: { hits: [ { item: { @@ -230,7 +230,7 @@ describe('Content Core', () => { const { fooContentCrud, ctx, cleanUp } = setup({ registerFooType: true }); const res = await fooContentCrud!.get(ctx, '1234'); - expect(res.item.item).toBeUndefined(); + expect(res.result.item).toBeUndefined(); await fooContentCrud!.create( ctx, { title: 'Hello' }, @@ -238,7 +238,7 @@ describe('Content Core', () => { ); expect(fooContentCrud!.get(ctx, '1234')).resolves.toEqual({ contentTypeId: FOO_CONTENT_ID, - item: { + result: { item: { id: '1234', title: 'Hello', @@ -256,7 +256,7 @@ describe('Content Core', () => { await fooContentCrud!.update(ctx, '1234', { title: 'changed' }); expect(fooContentCrud!.get(ctx, '1234')).resolves.toEqual({ contentTypeId: FOO_CONTENT_ID, - item: { + result: { item: { id: '1234', title: 'changed', @@ -292,7 +292,7 @@ describe('Content Core', () => { expect(fooContentCrud!.get(ctx, '1234')).resolves.toEqual({ contentTypeId: FOO_CONTENT_ID, - item: { + result: { item: { id: '1234', title: 'changed', @@ -309,14 +309,14 @@ describe('Content Core', () => { await fooContentCrud!.create(ctx, { title: 'Hello' }, { id: '1234' }); expect(fooContentCrud!.get(ctx, '1234')).resolves.toEqual({ contentTypeId: FOO_CONTENT_ID, - item: { + result: { item: expect.any(Object), }, }); await fooContentCrud!.delete(ctx, '1234'); expect(fooContentCrud!.get(ctx, '1234')).resolves.toEqual({ contentTypeId: FOO_CONTENT_ID, - item: { + result: { item: undefined, }, }); diff --git a/src/plugins/content_management/server/core/crud.ts b/src/plugins/content_management/server/core/crud.ts index 71fe8209bd8c6..21e13c1e6a698 100644 --- a/src/plugins/content_management/server/core/crud.ts +++ b/src/plugins/content_management/server/core/crud.ts @@ -19,12 +19,12 @@ import type { ContentStorage, StorageContext } from './types'; export interface GetResponse { contentTypeId: string; - item: GetResult; + result: GetResult; } export interface BulkGetResponse { contentTypeId: string; - items: BulkGetResult; + result: BulkGetResult; } export interface CreateItemResponse { @@ -89,7 +89,7 @@ export class ContentCrud { options, }); - return { contentTypeId: this.contentTypeId, item }; + return { contentTypeId: this.contentTypeId, result: item }; } catch (e) { this.eventBus.emit({ type: 'getItemError', @@ -128,7 +128,7 @@ export class ContentCrud { return { contentTypeId: this.contentTypeId, - items, + result: items, }; } catch (e) { this.eventBus.emit({ diff --git a/src/plugins/content_management/server/rpc/procedures/bulk_get.test.ts b/src/plugins/content_management/server/rpc/procedures/bulk_get.test.ts index e5783665d291b..ee83da4eea4a5 100644 --- a/src/plugins/content_management/server/rpc/procedures/bulk_get.test.ts +++ b/src/plugins/content_management/server/rpc/procedures/bulk_get.test.ts @@ -206,7 +206,7 @@ describe('RPC -> bulkGet()', () => { expect(result).toEqual({ contentTypeId: FOO_CONTENT_ID, - items: expected, + result: expected, }); expect(storage.bulkGet).toHaveBeenCalledWith( diff --git a/src/plugins/content_management/server/rpc/procedures/get.test.ts b/src/plugins/content_management/server/rpc/procedures/get.test.ts index b826e238ea26e..0e7fe59bb556a 100644 --- a/src/plugins/content_management/server/rpc/procedures/get.test.ts +++ b/src/plugins/content_management/server/rpc/procedures/get.test.ts @@ -161,7 +161,7 @@ describe('RPC -> get()', () => { expect(result).toEqual({ contentTypeId: FOO_CONTENT_ID, - item: expected, + result: expected, }); expect(storage.get).toHaveBeenCalledWith( diff --git a/src/plugins/content_management/server/rpc/procedures/search.test.ts b/src/plugins/content_management/server/rpc/procedures/search.test.ts index bd3a4ffd9fe06..880903a8decb8 100644 --- a/src/plugins/content_management/server/rpc/procedures/search.test.ts +++ b/src/plugins/content_management/server/rpc/procedures/search.test.ts @@ -7,15 +7,16 @@ */ import { omit } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { ContentManagementServiceDefinitionVersioned } from '@kbn/object-versioning'; +import type { SearchQuery } from '../../../common'; import { validate } from '../../utils'; import { ContentRegistry } from '../../core/registry'; import { createMockedStorage } from '../../core/mocks'; import { EventBus } from '../../core/event_bus'; -import { search } from './search'; -import { ContentManagementServiceDefinitionVersioned } from '@kbn/object-versioning'; -import { schema } from '@kbn/config-schema'; import { getServiceObjectTransformFactory } from '../services_transforms_factory'; +import { search } from './search'; const { fn, schemas } = search; @@ -34,7 +35,12 @@ const FOO_CONTENT_ID = 'foo'; describe('RPC -> search()', () => { describe('Input/Output validation', () => { - const query = { text: 'hello' }; + const query: SearchQuery = { + text: 'hello', + tags: { included: ['abc'], excluded: ['def'] }, + cursor: '1', + limit: 50, + }; const validInput = { contentTypeId: 'foo', version: 1, query }; test('should validate that a contentTypeId and "query" object is passed', () => { @@ -63,6 +69,10 @@ describe('RPC -> search()', () => { input: { ...validInput, query: 123 }, // query is not an object expectedError: '[query]: expected a plain object value, but found [number] instead.', }, + { + input: { ...validInput, query: { tags: { included: 123 } } }, // invalid query + expectedError: '[query.tags.included]: expected value of type [array] but got [number]', + }, { input: { ...validInput, unknown: 'foo' }, expectedError: '[unknown]: definition for this key is missing', 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 8e5a4556c7335..6bd8273efa1e6 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 @@ -30,7 +30,7 @@ export interface VisualizationListItem { export interface VisualizationsAppExtension { docTypes: string[]; searchFields?: string[]; - toListItem: (savedObject: SimpleSavedObject) => VisualizationListItem; + toListItem: (savedObject: SimpleSavedObject) => VisualizationListItem; } export interface VisTypeAlias { diff --git a/x-pack/plugins/maps/common/content_management/cm_services.ts b/x-pack/plugins/maps/common/content_management/cm_services.ts new file mode 100644 index 0000000000000..93914097529ec --- /dev/null +++ b/x-pack/plugins/maps/common/content_management/cm_services.ts @@ -0,0 +1,19 @@ +/* + * 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 { + ContentManagementServicesDefinition as ServicesDefinition, + Version, +} from '@kbn/object-versioning'; + +// We export the versionned service definition from this file and not the barrel to avoid adding +// the schemas in the "public" js bundle + +import { serviceDefinition as v1 } from './v1/cm_services'; + +export const cmServicesDefinition: { [version: Version]: ServicesDefinition } = { + 1: v1, +}; diff --git a/x-pack/plugins/maps/common/map_saved_object_type.ts b/x-pack/plugins/maps/common/content_management/constants.ts similarity index 52% rename from x-pack/plugins/maps/common/map_saved_object_type.ts rename to x-pack/plugins/maps/common/content_management/constants.ts index f16683f56ef6d..cbc5b1068eb0e 100644 --- a/x-pack/plugins/maps/common/map_saved_object_type.ts +++ b/x-pack/plugins/maps/common/content_management/constants.ts @@ -5,12 +5,6 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ +export const LATEST_VERSION = 1; -export type MapSavedObjectAttributes = { - title: string; - description?: string; - mapStateJSON?: string; - layerListJSON?: string; - uiStateJSON?: string; -}; +export const CONTENT_ID = 'map'; diff --git a/x-pack/plugins/maps/common/content_management/index.ts b/x-pack/plugins/maps/common/content_management/index.ts new file mode 100644 index 0000000000000..daafc194a37fc --- /dev/null +++ b/x-pack/plugins/maps/common/content_management/index.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +export { LATEST_VERSION, CONTENT_ID } from './constants'; + +export type { MapContentType } from './types'; + +export type { + MapAttributes, + MapItem, + PartialMapItem, + MapGetIn, + MapGetOut, + MapCreateIn, + MapCreateOut, + CreateOptions, + MapUpdateIn, + MapUpdateOut, + UpdateOptions, + MapDeleteIn, + MapDeleteOut, + MapSearchIn, + MapSearchOptions, + MapSearchOut, +} from './latest'; + +// Today "v1" === "latest" so the export under MapV1 namespace is not really useful +// We leave it as a reference for future version when it will be needed to export/support older types +// in the UIs. +export * as MapV1 from './v1'; diff --git a/x-pack/plugins/maps/common/content_management/latest.ts b/x-pack/plugins/maps/common/content_management/latest.ts new file mode 100644 index 0000000000000..b2f4f73793aa2 --- /dev/null +++ b/x-pack/plugins/maps/common/content_management/latest.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +// Latest version is 1 +export * from './v1'; diff --git a/x-pack/plugins/maps/common/content_management/types.ts b/x-pack/plugins/maps/common/content_management/types.ts new file mode 100644 index 0000000000000..f57924cae3d94 --- /dev/null +++ b/x-pack/plugins/maps/common/content_management/types.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export type MapContentType = 'map'; 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 new file mode 100644 index 0000000000000..0e430b56ced98 --- /dev/null +++ b/x-pack/plugins/maps/common/content_management/v1/cm_services.ts @@ -0,0 +1,137 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import type { ContentManagementServicesDefinition as ServicesDefinition } from '@kbn/object-versioning'; + +const apiError = schema.object({ + error: schema.string(), + message: schema.string(), + statusCode: schema.number(), + metadata: schema.object({}, { unknowns: 'allow' }), +}); + +const referenceSchema = schema.object( + { + name: schema.maybe(schema.string()), + type: schema.string(), + id: schema.string(), + }, + { unknowns: 'forbid' } +); + +const referencesSchema = schema.arrayOf(referenceSchema); + +const mapAttributesSchema = schema.object( + { + title: schema.string(), + description: schema.maybe(schema.string()), + mapStateJSON: schema.maybe(schema.string()), + layerListJSON: schema.maybe(schema.string()), + uiStateJSON: schema.maybe(schema.string()), + }, + { unknowns: 'forbid' } +); + +const mapSavedObjectSchema = schema.object( + { + id: schema.string(), + type: schema.string(), + version: schema.maybe(schema.string()), + createdAt: schema.maybe(schema.string()), + updatedAt: schema.maybe(schema.string()), + error: schema.maybe(apiError), + attributes: mapAttributesSchema, + references: referencesSchema, + namespaces: schema.maybe(schema.arrayOf(schema.string())), + originId: schema.maybe(schema.string()), + }, + { unknowns: 'allow' } +); + +const getResultSchema = schema.object( + { + item: mapSavedObjectSchema, + meta: schema.object( + { + outcome: schema.oneOf([ + schema.literal('exactMatch'), + schema.literal('aliasMatch'), + schema.literal('conflict'), + ]), + aliasTargetId: schema.maybe(schema.string()), + aliasPurpose: schema.maybe( + schema.oneOf([ + schema.literal('savedObjectConversion'), + schema.literal('savedObjectImport'), + ]) + ), + }, + { unknowns: 'forbid' } + ), + }, + { unknowns: 'forbid' } +); + +const createOptionsSchema = schema.object({ + references: schema.maybe(referencesSchema), +}); + +// Content management service definition. +// We need it for BWC support between different versions of the content +export const serviceDefinition: ServicesDefinition = { + get: { + out: { + result: { + schema: getResultSchema, + }, + }, + }, + create: { + in: { + options: { + schema: createOptionsSchema, + }, + data: { + schema: mapAttributesSchema, + }, + }, + out: { + result: { + schema: schema.object( + { + item: mapSavedObjectSchema, + }, + { unknowns: 'forbid' } + ), + }, + }, + }, + update: { + in: { + options: { + schema: createOptionsSchema, // same schema as "create" + }, + data: { + schema: mapAttributesSchema, + }, + }, + }, + search: { + in: { + options: { + schema: schema.maybe( + schema.object( + { + onlyTitle: schema.maybe(schema.boolean()), + }, + { unknowns: 'forbid' } + ) + ), + }, + }, + }, +}; diff --git a/x-pack/plugins/maps/common/content_management/v1/index.ts b/x-pack/plugins/maps/common/content_management/v1/index.ts new file mode 100644 index 0000000000000..272e0e1eb5f2e --- /dev/null +++ b/x-pack/plugins/maps/common/content_management/v1/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +export type { + MapAttributes, + MapItem, + PartialMapItem, + MapGetIn, + MapGetOut, + MapCreateIn, + MapCreateOut, + CreateOptions, + MapUpdateIn, + MapUpdateOut, + UpdateOptions, + MapDeleteIn, + MapDeleteOut, + MapSearchIn, + MapSearchOptions, + MapSearchOut, +} from './types'; diff --git a/x-pack/plugins/maps/common/content_management/v1/types.ts b/x-pack/plugins/maps/common/content_management/v1/types.ts new file mode 100644 index 0000000000000..d5a7351226c23 --- /dev/null +++ b/x-pack/plugins/maps/common/content_management/v1/types.ts @@ -0,0 +1,110 @@ +/* + * 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 { + GetIn, + GetResult, + CreateIn, + CreateResult, + SearchIn, + SearchResult, + UpdateIn, + UpdateResult, + DeleteIn, + DeleteResult, +} from '@kbn/content-management-plugin/common'; +import { MapContentType } from '../types'; + +interface Reference { + type: string; + id: string; + name: string; +} + +/* eslint-disable-next-line @typescript-eslint/consistent-type-definitions */ +export type MapAttributes = { + title: string; + description?: string; + mapStateJSON?: string; + layerListJSON?: string; + uiStateJSON?: string; +}; + +export interface MapItem { + id: string; + type: string; + version?: string; + createdAt?: string; + updatedAt?: string; + error?: { + error: string; + message: string; + statusCode: number; + metadata?: Record; + }; + attributes: MapAttributes; + references: Reference[]; + namespaces?: string[]; + originId?: string; +} + +export type PartialMapItem = Omit & { + attributes: Partial; + references: Reference[] | undefined; +}; + +// ----------- GET -------------- + +export type MapGetIn = GetIn; + +export type MapGetOut = GetResult< + MapItem, + { + outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; + aliasTargetId?: string; + aliasPurpose?: 'savedObjectConversion' | 'savedObjectImport'; + } +>; + +// ----------- CREATE -------------- + +export interface CreateOptions { + /** Array of referenced saved objects. */ + references?: Reference[]; +} + +export type MapCreateIn = CreateIn; + +export type MapCreateOut = CreateResult; + +// ----------- UPDATE -------------- + +export interface UpdateOptions { + /** Array of referenced saved objects. */ + references?: Reference[]; +} + +export type MapUpdateIn = UpdateIn; + +export type MapUpdateOut = UpdateResult; + +// ----------- DELETE -------------- + +export type MapDeleteIn = DeleteIn; + +export type MapDeleteOut = DeleteResult; + +// ----------- SEARCH -------------- + +export interface MapSearchOptions { + /** Flag to indicate to only search the text on the "title" field */ + onlyTitle?: boolean; +} + +export type MapSearchIn = SearchIn; + +export type MapSearchOut = SearchResult; diff --git a/x-pack/plugins/maps/common/embeddable/extract.ts b/x-pack/plugins/maps/common/embeddable/extract.ts index d329aefe7cff6..46ff3def8f0bc 100644 --- a/x-pack/plugins/maps/common/embeddable/extract.ts +++ b/x-pack/plugins/maps/common/embeddable/extract.ts @@ -7,7 +7,7 @@ import { EmbeddableRegistryDefinition } from '@kbn/embeddable-plugin/common'; import { MapEmbeddablePersistableState } from './types'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; import { extractReferences } from '../migrations/references'; export const extract: EmbeddableRegistryDefinition['extract'] = (state) => { @@ -21,7 +21,7 @@ export const extract: EmbeddableRegistryDefinition['extract'] = (state) => { // by-value embeddable const { attributes, references } = extractReferences({ - attributes: typedState.attributes as MapSavedObjectAttributes, + attributes: typedState.attributes as MapAttributes, }); return { diff --git a/x-pack/plugins/maps/common/embeddable/inject.ts b/x-pack/plugins/maps/common/embeddable/inject.ts index 4bb26dd00d28d..0fc0963e4be74 100644 --- a/x-pack/plugins/maps/common/embeddable/inject.ts +++ b/x-pack/plugins/maps/common/embeddable/inject.ts @@ -7,7 +7,7 @@ import type { EmbeddableRegistryDefinition } from '@kbn/embeddable-plugin/common'; import type { MapEmbeddablePersistableState } from './types'; -import type { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; import { extractReferences, injectReferences } from '../migrations/references'; export const inject: EmbeddableRegistryDefinition['inject'] = (state, references) => { @@ -23,7 +23,7 @@ export const inject: EmbeddableRegistryDefinition['inject'] = (state, references // run embeddable state through extract logic to ensure any state with hard coded ids is replace with refNames // refName generation will produce consistent values allowing inject logic to then replace refNames with current ids. const { attributes: attributesWithNoHardCodedIds } = extractReferences({ - attributes: typedState.attributes as MapSavedObjectAttributes, + attributes: typedState.attributes as MapAttributes, }); const { attributes: attributesWithInjectedIds } = injectReferences({ diff --git a/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts b/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts index b43b8979094bb..868e73fe0ca0b 100644 --- a/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts +++ b/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts @@ -5,18 +5,14 @@ * 2.0. */ -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; import { JoinDescriptor, LayerDescriptor, VectorLayerDescriptor } from '../descriptor_types'; import { SOURCE_TYPES } from '../constants'; // enforce type property on joins. It's possible older saved-objects do not have this correctly filled in // e.g. sample-data was missing the right.type field. // This is just to be safe. -export function addTypeToTermJoin({ - attributes, -}: { - attributes: MapSavedObjectAttributes; -}): MapSavedObjectAttributes { +export function addTypeToTermJoin({ attributes }: { attributes: MapAttributes }): MapAttributes { if (!attributes || !attributes.layerListJSON) { return attributes; } diff --git a/x-pack/plugins/maps/common/migrations/join_agg_key.ts b/x-pack/plugins/maps/common/migrations/join_agg_key.ts index ae102f2ed540f..f74722579ca5a 100644 --- a/x-pack/plugins/maps/common/migrations/join_agg_key.ts +++ b/x-pack/plugins/maps/common/migrations/join_agg_key.ts @@ -21,7 +21,7 @@ import { LayerDescriptor, VectorLayerDescriptor, } from '../descriptor_types'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; const GROUP_BY_DELIMITER = '_groupby_'; @@ -53,11 +53,7 @@ function parseLegacyAggKey(legacyAggKey: string): { aggType: AGG_TYPE; aggFieldN }; } -export function migrateJoinAggKey({ - attributes, -}: { - attributes: MapSavedObjectAttributes; -}): MapSavedObjectAttributes { +export function migrateJoinAggKey({ attributes }: { attributes: MapAttributes }): MapAttributes { if (!attributes || !attributes.layerListJSON) { return attributes; } diff --git a/x-pack/plugins/maps/common/migrations/migrate_data_persisted_state.ts b/x-pack/plugins/maps/common/migrations/migrate_data_persisted_state.ts index 0de5c1718910e..bb38a76c2e21e 100644 --- a/x-pack/plugins/maps/common/migrations/migrate_data_persisted_state.ts +++ b/x-pack/plugins/maps/common/migrations/migrate_data_persisted_state.ts @@ -7,16 +7,16 @@ import { Filter } from '@kbn/es-query'; import { MigrateFunction } from '@kbn/kibana-utils-plugin/common'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; export function migrateDataPersistedState( { attributes, }: { - attributes: MapSavedObjectAttributes; + attributes: MapAttributes; }, filterMigration: MigrateFunction -): MapSavedObjectAttributes { +): MapAttributes { let mapState: { filters: Filter[] } = { filters: [] }; if (attributes.mapStateJSON) { try { diff --git a/x-pack/plugins/maps/common/migrations/migrate_data_view_persisted_state.ts b/x-pack/plugins/maps/common/migrations/migrate_data_view_persisted_state.ts index 413afbdc245b9..6247320736a7f 100644 --- a/x-pack/plugins/maps/common/migrations/migrate_data_view_persisted_state.ts +++ b/x-pack/plugins/maps/common/migrations/migrate_data_view_persisted_state.ts @@ -8,16 +8,16 @@ import type { Serializable } from '@kbn/utility-types'; import type { DataViewSpec } from '@kbn/data-plugin/common'; import { MigrateFunction } from '@kbn/kibana-utils-plugin/common'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; export function migrateDataViewsPersistedState( { attributes, }: { - attributes: MapSavedObjectAttributes; + attributes: MapAttributes; }, migration: MigrateFunction -): MapSavedObjectAttributes { +): MapAttributes { let mapState: { adHocDataViews?: DataViewSpec[] } = { adHocDataViews: [] }; if (attributes.mapStateJSON) { try { diff --git a/x-pack/plugins/maps/common/migrations/migrate_other_category_color.ts b/x-pack/plugins/maps/common/migrations/migrate_other_category_color.ts index eaa1953efb8e6..c53f4e6409fcc 100644 --- a/x-pack/plugins/maps/common/migrations/migrate_other_category_color.ts +++ b/x-pack/plugins/maps/common/migrations/migrate_other_category_color.ts @@ -12,7 +12,7 @@ import { LayerDescriptor, VectorStyleDescriptor, } from '../descriptor_types'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; const COLOR_STYLES = [ VECTOR_STYLES.FILL_COLOR, @@ -53,8 +53,8 @@ function migrateColorProperty( export function migrateOtherCategoryColor({ attributes, }: { - attributes: MapSavedObjectAttributes; -}): MapSavedObjectAttributes { + attributes: MapAttributes; +}): MapAttributes { if (!attributes || !attributes.layerListJSON) { return attributes; } diff --git a/x-pack/plugins/maps/common/migrations/move_attribution.ts b/x-pack/plugins/maps/common/migrations/move_attribution.ts index 6ab5fb93ca981..66aff324ed370 100644 --- a/x-pack/plugins/maps/common/migrations/move_attribution.ts +++ b/x-pack/plugins/maps/common/migrations/move_attribution.ts @@ -5,15 +5,11 @@ * 2.0. */ -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; import { LayerDescriptor } from '../descriptor_types'; // In 7.14, attribution added to the layer_descriptor. Prior to 7.14, 2 sources, WMS and TMS, had attribution on source descriptor. -export function moveAttribution({ - attributes, -}: { - attributes: MapSavedObjectAttributes; -}): MapSavedObjectAttributes { +export function moveAttribution({ attributes }: { attributes: MapAttributes }): MapAttributes { if (!attributes || !attributes.layerListJSON) { return attributes; } diff --git a/x-pack/plugins/maps/common/migrations/references.ts b/x-pack/plugins/maps/common/migrations/references.ts index fc94236583470..07848ed4611ae 100644 --- a/x-pack/plugins/maps/common/migrations/references.ts +++ b/x-pack/plugins/maps/common/migrations/references.ts @@ -9,7 +9,7 @@ import type { DataViewSpec } from '@kbn/data-plugin/common'; import { SavedObjectReference } from '@kbn/core/types'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; import { LayerDescriptor, VectorLayerDescriptor } from '../descriptor_types'; interface IndexPatternReferenceDescriptor { @@ -21,7 +21,7 @@ export function extractReferences({ attributes, references = [], }: { - attributes: MapSavedObjectAttributes; + attributes: MapAttributes; references?: SavedObjectReference[]; }) { if (!attributes.layerListJSON) { @@ -119,7 +119,7 @@ export function injectReferences({ attributes, references, }: { - attributes: MapSavedObjectAttributes; + attributes: MapAttributes; references: SavedObjectReference[]; }) { if (!attributes.layerListJSON) { diff --git a/x-pack/plugins/maps/common/migrations/remove_bounds.ts b/x-pack/plugins/maps/common/migrations/remove_bounds.ts index 08858aeee64af..a91766ad4556c 100644 --- a/x-pack/plugins/maps/common/migrations/remove_bounds.ts +++ b/x-pack/plugins/maps/common/migrations/remove_bounds.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; export function removeBoundsFromSavedObject({ attributes, }: { - attributes: MapSavedObjectAttributes; -}): MapSavedObjectAttributes { + attributes: MapAttributes; +}): MapAttributes { const newAttributes = { ...attributes }; // @ts-expect-error // This removes an unused parameter from pre 7.8=< saved objects diff --git a/x-pack/plugins/maps/common/migrations/rename_layer_types.ts b/x-pack/plugins/maps/common/migrations/rename_layer_types.ts index 6ba924ff32268..07965654752ff 100644 --- a/x-pack/plugins/maps/common/migrations/rename_layer_types.ts +++ b/x-pack/plugins/maps/common/migrations/rename_layer_types.ts @@ -7,18 +7,14 @@ import { LAYER_TYPE } from '../constants'; import { LayerDescriptor } from '../descriptor_types'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; // LAYER_TYPE constants renamed in 8.1 to provide more distinguishable names that better refect layer. // TILED_VECTOR replaced with MVT_VECTOR // VECTOR_TILE replaced with EMS_VECTOR_TILE // VECTOR replaced with GEOJSON_VECTOR // TILE replaced with RASTER_TILE -export function renameLayerTypes({ - attributes, -}: { - attributes: MapSavedObjectAttributes; -}): MapSavedObjectAttributes { +export function renameLayerTypes({ attributes }: { attributes: MapAttributes }): MapAttributes { if (!attributes || !attributes.layerListJSON) { return attributes; } diff --git a/x-pack/plugins/maps/common/migrations/scaling_type.ts b/x-pack/plugins/maps/common/migrations/scaling_type.ts index 5106784a16d0a..25c0e666985a9 100644 --- a/x-pack/plugins/maps/common/migrations/scaling_type.ts +++ b/x-pack/plugins/maps/common/migrations/scaling_type.ts @@ -8,7 +8,7 @@ import _ from 'lodash'; import { SOURCE_TYPES, SCALING_TYPES } from '../constants'; import { LayerDescriptor, ESSearchSourceDescriptor } from '../descriptor_types'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; function isEsDocumentSource(layerDescriptor: LayerDescriptor) { const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); @@ -18,8 +18,8 @@ function isEsDocumentSource(layerDescriptor: LayerDescriptor) { export function migrateUseTopHitsToScalingType({ attributes, }: { - attributes: MapSavedObjectAttributes; -}): MapSavedObjectAttributes { + attributes: MapAttributes; +}): MapAttributes { if (!attributes || !attributes.layerListJSON) { return attributes; } diff --git a/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts b/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts index af86b62165683..561683b13310b 100644 --- a/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts +++ b/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; export function setDefaultAutoFitToBounds({ attributes, }: { - attributes: MapSavedObjectAttributes; -}): MapSavedObjectAttributes { + attributes: MapAttributes; +}): MapAttributes { if (!attributes || !attributes.mapStateJSON) { return attributes; } diff --git a/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts b/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts index aab0d6b345428..e31eb02fbc262 100644 --- a/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts +++ b/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts @@ -7,7 +7,7 @@ import { SOURCE_TYPES } from '../constants'; import { LayerDescriptor, EMSTMSSourceDescriptor } from '../descriptor_types'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; // LightModeDefault added to EMSTMSSourceDescriptor in 8.0.0 // to avoid changing auto selected light mode tiles for maps created < 8.0.0 @@ -16,8 +16,8 @@ import { MapSavedObjectAttributes } from '../map_saved_object_type'; export function setEmsTmsDefaultModes({ attributes, }: { - attributes: MapSavedObjectAttributes; -}): MapSavedObjectAttributes { + attributes: MapAttributes; +}): MapAttributes { if (!attributes || !attributes.layerListJSON) { return attributes; } diff --git a/x-pack/plugins/maps/common/telemetry/layer_stats_collector.test.ts b/x-pack/plugins/maps/common/telemetry/layer_stats_collector.test.ts index 1b9c6adfa2eb5..ff216c3cfd64a 100644 --- a/x-pack/plugins/maps/common/telemetry/layer_stats_collector.test.ts +++ b/x-pack/plugins/maps/common/telemetry/layer_stats_collector.test.ts @@ -8,7 +8,7 @@ // @ts-ignore import mapSavedObjects from './test_resources/sample_map_saved_objects.json'; import { LayerStatsCollector } from './layer_stats_collector'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; const expecteds = [ { @@ -69,7 +69,7 @@ const expecteds = [ ]; const testsToRun = mapSavedObjects.map( - (savedObject: { attributes: MapSavedObjectAttributes }, index: number) => { + (savedObject: { attributes: MapAttributes }, index: number) => { const { attributes } = savedObject; return [attributes, expecteds[index]] as const; } diff --git a/x-pack/plugins/maps/common/telemetry/layer_stats_collector.ts b/x-pack/plugins/maps/common/telemetry/layer_stats_collector.ts index e31f4120194f6..4097c58377ef8 100644 --- a/x-pack/plugins/maps/common/telemetry/layer_stats_collector.ts +++ b/x-pack/plugins/maps/common/telemetry/layer_stats_collector.ts @@ -19,7 +19,7 @@ import { LayerDescriptor, VectorLayerDescriptor, } from '../descriptor_types'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; import { EMS_BASEMAP_KEYS, JOIN_KEYS, LAYER_KEYS, RESOLUTION_KEYS, SCALING_KEYS } from './types'; export class LayerStatsCollector { @@ -34,7 +34,7 @@ export class LayerStatsCollector { private _layerTypeCounts: { [key: string]: number } = {}; private _sourceIds: Set = new Set(); - constructor(attributes: MapSavedObjectAttributes) { + constructor(attributes: MapAttributes) { if (!attributes || !attributes.layerListJSON) { return; } diff --git a/x-pack/plugins/maps/common/telemetry/map_settings_collector.test.ts b/x-pack/plugins/maps/common/telemetry/map_settings_collector.test.ts index 91086641beca8..3a92381d0bb69 100644 --- a/x-pack/plugins/maps/common/telemetry/map_settings_collector.test.ts +++ b/x-pack/plugins/maps/common/telemetry/map_settings_collector.test.ts @@ -8,7 +8,7 @@ // @ts-ignore import mapSavedObjects from './test_resources/sample_map_saved_objects.json'; import { MapSettingsCollector } from './map_settings_collector'; -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; const expecteds = [ { @@ -30,7 +30,7 @@ const expecteds = [ ]; const testsToRun = mapSavedObjects.map( - (savedObject: { attributes: MapSavedObjectAttributes }, index: number) => { + (savedObject: { attributes: MapAttributes }, index: number) => { const { attributes } = savedObject; return [attributes, expecteds[index]] as const; } diff --git a/x-pack/plugins/maps/common/telemetry/map_settings_collector.ts b/x-pack/plugins/maps/common/telemetry/map_settings_collector.ts index b310b69f6fa36..905d82ae882a5 100644 --- a/x-pack/plugins/maps/common/telemetry/map_settings_collector.ts +++ b/x-pack/plugins/maps/common/telemetry/map_settings_collector.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import type { MapAttributes } from '../content_management'; import { MapSettings } from '../descriptor_types'; export class MapSettingsCollector { private _customIconsCount: number = 0; - constructor(attributes: MapSavedObjectAttributes) { + constructor(attributes: MapAttributes) { if (!attributes || !attributes.mapStateJSON) { return; } diff --git a/x-pack/plugins/maps/kibana.jsonc b/x-pack/plugins/maps/kibana.jsonc index 416bcf32eb3f8..bfb15e72828a2 100644 --- a/x-pack/plugins/maps/kibana.jsonc +++ b/x-pack/plugins/maps/kibana.jsonc @@ -29,7 +29,8 @@ "mapsEms", "savedObjects", "share", - "presentationUtil" + "presentationUtil", + "contentManagement" ], "optionalPlugins": [ "cloud", 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 new file mode 100644 index 0000000000000..eb8f4760774ce --- /dev/null +++ b/x-pack/plugins/maps/public/content_management/duplicate_title_check.ts @@ -0,0 +1,67 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { OverlayStart } from '@kbn/core/public'; + +import { mapsClient } from './maps_client'; + +const rejectErrorMessage = i18n.translate('xpack.maps.saveDuplicateRejectedDescription', { + defaultMessage: 'Save with duplicate title confirmation was rejected', +}); + +interface Props { + title: string; + id?: string; + getDisplayName: () => string; + onTitleDuplicate: () => void; + lastSavedTitle: string; + copyOnSave: boolean; + isTitleDuplicateConfirmed: boolean; +} + +interface Context { + overlays: OverlayStart; +} + +export const checkForDuplicateTitle = async ( + { + id, + title, + lastSavedTitle, + copyOnSave, + isTitleDuplicateConfirmed, + getDisplayName, + onTitleDuplicate, + }: Props, + { overlays }: Context +) => { + if (isTitleDuplicateConfirmed) { + return true; + } + + if (title === lastSavedTitle && !copyOnSave) { + return true; + } + + const { hits } = await mapsClient.search( + { + text: `"${title}"`, + limit: 10, + }, + { onlyTitle: true } + ); + + const existing = hits.find((obj) => obj.attributes.title.toLowerCase() === title.toLowerCase()); + + if (!existing || existing.id === id) { + return true; + } + + onTitleDuplicate(); + return Promise.reject(new Error(rejectErrorMessage)); +}; diff --git a/x-pack/plugins/maps/public/content_management/index.ts b/x-pack/plugins/maps/public/content_management/index.ts new file mode 100644 index 0000000000000..8067e4250c9f1 --- /dev/null +++ b/x-pack/plugins/maps/public/content_management/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export { mapsClient } 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 new file mode 100644 index 0000000000000..932765899da22 --- /dev/null +++ b/x-pack/plugins/maps/public/content_management/maps_client.ts @@ -0,0 +1,71 @@ +/* + * 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 { + MapGetIn, + MapGetOut, + MapCreateIn, + MapCreateOut, + MapUpdateIn, + MapUpdateOut, + MapDeleteIn, + MapDeleteOut, + MapSearchIn, + MapSearchOut, + MapSearchOptions, +} from '../../common/content_management'; +import { getContentManagement } from '../kibana_services'; + +const get = async (id: string) => { + return getContentManagement().client.get({ + contentTypeId: 'map', + id, + }); +}; + +const create = async ({ data, options }: Omit) => { + const res = await getContentManagement().client.create({ + contentTypeId: 'map', + data, + options, + }); + return res; +}; + +const update = async ({ id, data, options }: Omit) => { + const res = await getContentManagement().client.update({ + contentTypeId: 'map', + id, + data, + options, + }); + return res; +}; + +const deleteMap = async (id: string) => { + await getContentManagement().client.delete({ + contentTypeId: 'map', + id, + }); +}; + +const search = async (query: SearchQuery = {}, options?: MapSearchOptions) => { + return getContentManagement().client.search({ + contentTypeId: 'map', + query, + options, + }); +}; + +export const mapsClient = { + get, + create, + update, + delete: deleteMap, + search, +}; diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.test.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.test.tsx index 1528290dcde2f..f650b4e077199 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.test.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.test.tsx @@ -11,7 +11,7 @@ import { getControlledBy, MapEmbeddable } from './map_embeddable'; import { buildExistsFilter, disableFilter, pinFilter, toggleFilterNegated } from '@kbn/es-query'; import type { DataViewFieldBase, DataViewBase } from '@kbn/es-query'; import { MapEmbeddableConfig, MapEmbeddableInput } from './types'; -import { MapSavedObjectAttributes } from '../../common/map_saved_object_type'; +import type { MapAttributes } from '../../common/content_management'; jest.mock('../kibana_services', () => { return { @@ -66,7 +66,7 @@ jest.mock('../routes/map_page', () => { class MockSavedMap { // eslint-disable-next-line @typescript-eslint/no-var-requires private _store = require('../reducers/store').createMapStore(); - private _attributes: MapSavedObjectAttributes = { + private _attributes: MapAttributes = { title: 'myMap', }; diff --git a/x-pack/plugins/maps/public/embeddable/types.ts b/x-pack/plugins/maps/public/embeddable/types.ts index bd5e1621e18f0..95b37b32048b2 100644 --- a/x-pack/plugins/maps/public/embeddable/types.ts +++ b/x-pack/plugins/maps/public/embeddable/types.ts @@ -15,7 +15,7 @@ import { } from '@kbn/embeddable-plugin/public'; import type { Filter, Query, TimeRange } from '@kbn/es-query'; import { MapCenterAndZoom, MapExtent, MapSettings } from '../../common/descriptor_types'; -import { MapSavedObjectAttributes } from '../../common/map_saved_object_type'; +import type { MapAttributes } from '../../common/content_management'; export interface MapEmbeddableConfig { editable: boolean; @@ -37,7 +37,7 @@ interface MapEmbeddableState { isMovementSynchronized?: boolean; } export type MapByValueInput = { - attributes: MapSavedObjectAttributes; + attributes: MapAttributes; } & EmbeddableInput & MapEmbeddableState; export type MapByReferenceInput = SavedObjectEmbeddableInput & MapEmbeddableState; diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index b9c64946f7fa3..315f75c313fa5 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -44,7 +44,6 @@ export const getHttp = () => coreStart.http; export const getExecutionContextService = () => coreStart.executionContext; export const getTimeFilter = () => pluginsStart.data.query.timefilter.timefilter; export const getToasts = () => coreStart.notifications.toasts; -export const getSavedObjectsClient = () => coreStart.savedObjects.client; export const getCoreChrome = () => coreStart.chrome; export const getDevToolsCapabilities = () => coreStart.application.capabilities.dev_tools; export const getMapsCapabilities = () => coreStart.application.capabilities.maps; @@ -68,6 +67,7 @@ export const getSpacesApi = () => pluginsStart.spaces; export const getTheme = () => coreStart.theme; export const getApplication = () => coreStart.application; export const getUsageCollection = () => pluginsStart.usageCollection; +export const getContentManagement = () => pluginsStart.contentManagement; export const isScreenshotMode = () => { return pluginsStart.screenshotMode ? pluginsStart.screenshotMode.isScreenshotMode() : false; }; diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index ce1d59f5b328c..b93c5421f5cb3 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -8,12 +8,13 @@ import { SavedObjectReference } from '@kbn/core/types'; import type { ResolvedSimpleSavedObject } from '@kbn/core/public'; import { AttributeService } from '@kbn/embeddable-plugin/public'; -import { checkForDuplicateTitle, OnSaveProps } from '@kbn/saved-objects-plugin/public'; -import { MapSavedObjectAttributes } from '../common/map_saved_object_type'; +import type { OnSaveProps } from '@kbn/saved-objects-plugin/public'; +import type { MapAttributes } from '../common/content_management'; import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { getMapEmbeddableDisplayName } from '../common/i18n_getters'; -import { getCoreOverlays, getEmbeddableService, getSavedObjectsClient } from './kibana_services'; +import { getCoreOverlays, getEmbeddableService } from './kibana_services'; import { extractReferences, injectReferences } from '../common/migrations/references'; +import { mapsClient, checkForDuplicateTitle } from './content_management'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; export interface SharingSavedObjectProps { @@ -23,7 +24,7 @@ export interface SharingSavedObjectProps { sourceId?: string; } -type MapDoc = MapSavedObjectAttributes & { +type MapDoc = MapAttributes & { references?: SavedObjectReference[]; }; export interface MapUnwrapMetaInfo { @@ -62,19 +63,12 @@ export function getMapAttributeService(): MapAttributeService { references: savedObjectClientReferences, }); - const savedObject = await (savedObjectId - ? getSavedObjectsClient().update( - MAP_SAVED_OBJECT_TYPE, - savedObjectId, - updatedAttributes, - { references } - ) - : getSavedObjectsClient().create( - MAP_SAVED_OBJECT_TYPE, - updatedAttributes, - { references } - )); - return { id: savedObject.id }; + const { + item: { id }, + } = await (savedObjectId + ? mapsClient.update({ id: savedObjectId, data: updatedAttributes, options: { references } }) + : mapsClient.create({ data: updatedAttributes, options: { references } })); + return { id }; }, unwrapMethod: async ( savedObjectId: string @@ -83,14 +77,9 @@ export function getMapAttributeService(): MapAttributeService { metaInfo: MapUnwrapMetaInfo; }> => { const { - saved_object: savedObject, - outcome, - alias_target_id: aliasTargetId, - alias_purpose: aliasPurpose, - } = await getSavedObjectsClient().resolve( - MAP_SAVED_OBJECT_TYPE, - savedObjectId - ); + item: savedObject, + meta: { outcome, aliasPurpose, aliasTargetId }, + } = await mapsClient.get(savedObjectId); if (savedObject.error) { throw savedObject.error; @@ -118,13 +107,11 @@ export function getMapAttributeService(): MapAttributeService { title: props.newTitle, copyOnSave: false, lastSavedTitle: '', - getEsType: () => MAP_SAVED_OBJECT_TYPE, + isTitleDuplicateConfirmed: props.isTitleDuplicateConfirmed, getDisplayName: getMapEmbeddableDisplayName, + onTitleDuplicate: props.onTitleDuplicate, }, - props.isTitleDuplicateConfirmed, - props.onTitleDuplicate, { - savedObjectsClient: getSavedObjectsClient(), overlays: getCoreOverlays(), } ); 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 e2506106ed6ec..d26108737ab33 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.ts +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.ts @@ -7,8 +7,7 @@ import { i18n } from '@kbn/i18n'; import type { VisualizationsSetup, VisualizationStage } from '@kbn/visualizations-plugin/public'; -import type { SimpleSavedObject } from '@kbn/core/public'; -import type { MapSavedObjectAttributes } from '../common/map_saved_object_type'; +import type { MapItem } from '../common/content_management'; import { APP_ID, APP_ICON, @@ -37,9 +36,8 @@ export function getMapsVisTypeAlias(visualizations: VisualizationsSetup) { visualizations: { docTypes: [MAP_SAVED_OBJECT_TYPE], searchFields: ['title^3'], - toListItem(savedObject: SimpleSavedObject) { - const { id, type, updatedAt, attributes } = - savedObject as SimpleSavedObject; + toListItem(mapItem: MapItem) { + const { id, type, updatedAt, attributes } = mapItem; const { title, description } = attributes; return { diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index a59d31c6b430b..0d558699e6e51 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -33,7 +33,6 @@ import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plu import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { FileUploadPluginStart } from '@kbn/file-upload-plugin/public'; -import type { SavedObjectsStart } from '@kbn/saved-objects-plugin/public'; import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; import type { ChartsPluginStart } from '@kbn/charts-plugin/public'; @@ -42,6 +41,11 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import type { CloudSetup } from '@kbn/cloud-plugin/public'; import type { LensPublicSetup } from '@kbn/lens-plugin/public'; import { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public'; +import type { + ContentManagementPublicSetup, + ContentManagementPublicStart, +} from '@kbn/content-management-plugin/public'; + import { createRegionMapFn, GEOHASH_GRID, @@ -80,6 +84,7 @@ import { setIsCloudEnabled, setMapAppConfig, setStartServices } from './kibana_s import { MapInspectorView, VectorTileInspectorView } from './inspector'; import { setupLensChoroplethChart } from './lens'; +import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; export interface MapsPluginSetupDependencies { cloud?: CloudSetup; @@ -94,6 +99,7 @@ export interface MapsPluginSetupDependencies { licensing: LicensingPluginSetup; usageCollection?: UsageCollectionSetup; screenshotMode?: ScreenshotModePluginSetup; + contentManagement: ContentManagementPublicSetup; } export interface MapsPluginStartDependencies { @@ -109,13 +115,13 @@ export interface MapsPluginStartDependencies { uiActions: UiActionsStart; share: SharePluginStart; visualizations: VisualizationsStart; - savedObjects: SavedObjectsStart; dashboard: DashboardStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; presentationUtil: PresentationUtilPluginStart; security?: SecurityPluginStart; spaces?: SpacesPluginStart; mapsEms: MapsEmsPluginPublicStart; + contentManagement: ContentManagementPublicStart; screenshotMode?: ScreenshotModePluginSetup; usageCollection?: UsageCollectionSetup; } @@ -201,6 +207,14 @@ export class MapsPlugin }, }); + plugins.contentManagement.registry.register({ + id: CONTENT_ID, + version: { + latest: LATEST_VERSION, + }, + name: getAppTitle(), + }); + setupLensChoroplethChart(core, plugins.expressions, plugins.lens); // register wrapper around legacy tile_map and region_map visualizations 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 62403cd4db327..26cb872006dee 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 @@ -10,9 +10,10 @@ import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import { ScopedHistory } from '@kbn/core/public'; -import { getSavedObjectsClient, getToasts } from '../../kibana_services'; +import { getToasts } from '../../kibana_services'; import { MapsListView } from './maps_list_view'; -import { APP_ID, MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { APP_ID } from '../../../common/constants'; +import { mapsClient } from '../../content_management'; interface Props { history: ScopedHistory; @@ -38,13 +39,9 @@ export class LoadListAndRender extends Component { async _loadMapsList() { try { - const results = await getSavedObjectsClient().find({ - type: MAP_SAVED_OBJECT_TYPE, - perPage: 1, - fields: ['title'], - }); + const results = await mapsClient.search({ limit: 1 }); if (this._isMounted) { - this.setState({ mapsLoaded: true, hasSavedMaps: !!results.savedObjects.length }); + this.setState({ mapsLoaded: true, hasSavedMaps: !!results.hits.length }); } } catch (err) { if (this._isMounted) { 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 759189f509db0..e1ce3e801aac3 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 @@ -5,37 +5,29 @@ * 2.0. */ -import React from 'react'; -import { SavedObjectReference } from '@kbn/core/types'; +import React, { useCallback, memo } from 'react'; import type { SavedObjectsFindOptionsReference, ScopedHistory } from '@kbn/core/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; import { TableListView } from '@kbn/content-management-table-list'; import type { UserContentCommonSchema } from '@kbn/content-management-table-list'; -import { SimpleSavedObject } from '@kbn/core-saved-objects-api-browser'; -import { APP_ID, getEditPath, MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; + +import type { MapItem } from '../../../common/content_management'; +import { APP_ID, getEditPath, MAP_PATH } from '../../../common/constants'; import { getMapsCapabilities, getCoreChrome, getExecutionContextService, getNavigateToApp, - getSavedObjectsClient, getUiSettings, getUsageCollection, } from '../../kibana_services'; import { getAppTitle } from '../../../common/i18n_getters'; -import { MapSavedObjectAttributes } from '../../../common/map_saved_object_type'; +import { mapsClient } from '../../content_management'; const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit'; const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage'; -interface MapItem { - id: string; - title: string; - description?: string; - references?: SavedObjectReference[]; -} - interface MapUserContent extends UserContentCommonSchema { type: string; attributes: { @@ -51,59 +43,26 @@ function navigateToNewMap() { }); } -const toTableListViewSavedObject = ( - savedObject: SimpleSavedObject -): MapUserContent => { +const toTableListViewSavedObject = (mapItem: MapItem): MapUserContent => { return { - ...savedObject, - updatedAt: savedObject.updatedAt!, + ...mapItem, + updatedAt: mapItem.updatedAt!, attributes: { - ...savedObject.attributes, - title: savedObject.attributes.title ?? '', + ...mapItem.attributes, + title: mapItem.attributes.title ?? '', }, }; }; -async function findMaps( - searchTerm: string, - { - references, - referencesToExclude, - }: { - references?: SavedObjectsFindOptionsReference[]; - referencesToExclude?: SavedObjectsFindOptionsReference[]; - } = {} -) { - const resp = await getSavedObjectsClient().find({ - type: MAP_SAVED_OBJECT_TYPE, - search: searchTerm ? `${searchTerm}*` : undefined, - perPage: getUiSettings().get(SAVED_OBJECTS_LIMIT_SETTING), - page: 1, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - fields: ['description', 'title'], - hasReference: references, - hasNoReference: referencesToExclude, - }); - - return { - total: resp.total, - hits: resp.savedObjects.map(toTableListViewSavedObject), - }; -} - -async function deleteMaps(items: object[]) { - const deletions = items.map((item) => { - return getSavedObjectsClient().delete(MAP_SAVED_OBJECT_TYPE, (item as MapItem).id); - }); - await Promise.all(deletions); +async function deleteMaps(items: Array<{ id: string }>) { + await Promise.all(items.map(({ id }) => mapsClient.delete(id))); } interface Props { history: ScopedHistory; } -export function MapsListView(props: Props) { +function MapsListViewComp({ history }: Props) { getExecutionContextService().set({ type: 'application', name: APP_ID, @@ -117,6 +76,42 @@ export function MapsListView(props: Props) { getCoreChrome().docTitle.change(getAppTitle()); getCoreChrome().setBreadcrumbs([{ text: getAppTitle() }]); + const findMaps = useCallback( + async ( + searchTerm: string, + { + references = [], + referencesToExclude = [], + }: { + references?: SavedObjectsFindOptionsReference[]; + referencesToExclude?: SavedObjectsFindOptionsReference[]; + } = {} + ) => { + return mapsClient + .search({ + text: searchTerm ? `${searchTerm}*` : undefined, + limit: getUiSettings().get(SAVED_OBJECTS_LIMIT_SETTING), + tags: { + included: references.map(({ id }) => id), + excluded: referencesToExclude.map(({ id }) => id), + }, + }) + .then(({ hits, pagination: { total } }) => { + return { + total, + hits: hits.map(toTableListViewSavedObject), + }; + }) + .catch((e) => { + return { + total: 0, + hits: [], + }; + }); + }, + [] + ); + return ( id="map" @@ -134,7 +129,9 @@ export function MapsListView(props: Props) { defaultMessage: 'maps', })} tableListTitle={getAppTitle()} - onClickTitle={({ id }) => props.history.push(getEditPath(id))} + onClickTitle={({ id }) => history.push(getEditPath(id))} /> ); } + +export const MapsListView = memo(MapsListViewComp); diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index 30ce717b014dc..db9cd039071ae 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import { ScopedHistory } from '@kbn/core/public'; import { OnSaveProps } from '@kbn/saved-objects-plugin/public'; -import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; +import type { MapAttributes } from '../../../../common/content_management'; import { APP_ID, MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; import { createMapStore, MapStore, MapStoreState } from '../../../reducers/store'; import { MapSettings } from '../../../../common/descriptor_types'; @@ -71,7 +71,7 @@ function setMapSettingsFromEncodedState(settings: Partial) { } export class SavedMap { - private _attributes: MapSavedObjectAttributes | null = null; + private _attributes: MapAttributes | null = null; private _sharingSavedObjectProps: SharingSavedObjectProps | null = null; private readonly _defaultLayers: LayerDescriptor[]; private readonly _embeddableId?: string; @@ -385,7 +385,7 @@ export class SavedMap { return this._attributes.title !== undefined ? this._attributes.title : ''; } - public getAttributes(): MapSavedObjectAttributes { + public getAttributes(): MapAttributes { if (!this._attributes) { throw new Error('Invalid usage, must await whenReady before calling getAttributes'); } diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 2204d38483f49..8b7efa9335858 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { Adapters } from '@kbn/inspector-plugin/public'; import { - checkForDuplicateTitle, SavedObjectSaveModalOrigin, OnSaveProps, showSaveModal, @@ -24,14 +23,13 @@ import { getMapsCapabilities, getIsAllowByValueEmbeddables, getInspector, - getSavedObjectsClient, getCoreOverlays, getSavedObjectsTagging, getPresentationUtilContext, } from '../../kibana_services'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { SavedMap } from './saved_map'; import { getMapEmbeddableDisplayName } from '../../../common/i18n_getters'; +import { checkForDuplicateTitle } from '../../content_management'; const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); @@ -180,13 +178,11 @@ export function getTopNavConfig({ title: props.newTitle, copyOnSave: props.newCopyOnSave, lastSavedTitle: savedMap.getSavedObjectId() ? savedMap.getTitle() : '', - getEsType: () => MAP_SAVED_OBJECT_TYPE, + isTitleDuplicateConfirmed: props.isTitleDuplicateConfirmed, getDisplayName: getMapEmbeddableDisplayName, + onTitleDuplicate: props.onTitleDuplicate, }, - props.isTitleDuplicateConfirmed, - props.onTitleDuplicate, { - savedObjectsClient: getSavedObjectsClient(), overlays: getCoreOverlays(), } ); diff --git a/x-pack/plugins/maps/server/content_management/index.ts b/x-pack/plugins/maps/server/content_management/index.ts new file mode 100644 index 0000000000000..ff740b7374714 --- /dev/null +++ b/x-pack/plugins/maps/server/content_management/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { MapsStorage } from './maps_storage'; diff --git a/x-pack/plugins/maps/server/content_management/maps_storage.ts b/x-pack/plugins/maps/server/content_management/maps_storage.ts new file mode 100644 index 0000000000000..7633c2283fef9 --- /dev/null +++ b/x-pack/plugins/maps/server/content_management/maps_storage.ts @@ -0,0 +1,313 @@ +/* + * 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 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 { CONTENT_ID } from '../../common/content_management'; +import { cmServicesDefinition } from '../../common/content_management/cm_services'; +import type { + MapItem, + PartialMapItem, + MapContentType, + MapAttributes, + MapGetOut, + MapCreateIn, + MapCreateOut, + CreateOptions, + MapUpdateIn, + MapUpdateOut, + UpdateOptions, + MapDeleteOut, + MapSearchOptions, + MapSearchOut, +} 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 savedObjectToMapItem(savedObject: SavedObject, partial: false): MapItem; + +function savedObjectToMapItem( + savedObject: PartialSavedObject, + partial: true +): PartialMapItem; + +function savedObjectToMapItem( + savedObject: SavedObject | PartialSavedObject +): MapItem | PartialMapItem { + const { + id, + type, + updated_at: updatedAt, + created_at: createdAt, + attributes: { title, description, layerListJSON, mapStateJSON, uiStateJSON }, + references, + error, + namespaces, + } = savedObject; + + return { + id, + type, + updatedAt, + createdAt, + attributes: { + title, + description, + layerListJSON, + mapStateJSON, + uiStateJSON, + }, + references, + error, + namespaces, + }; +} + +const SO_TYPE: MapContentType = 'map'; + +export class MapsStorage implements ContentStorage { + constructor() {} + + 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: MapGetOut = { + item: savedObjectToMapItem(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. Maps does not use bulkGet + throw new Error(`[bulkGet] has not been implemented. See MapsStorage class.`); + } + + async create( + ctx: StorageContext, + data: MapCreateIn['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< + MapAttributes, + MapAttributes + >(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< + MapCreateOut, + MapCreateOut + >({ + item: savedObjectToMapItem(savedObject, false), + }); + + if (resultError) { + throw Boom.badRequest(`Invalid response. ${resultError.message}`); + } + + return value; + } + + async update( + ctx: StorageContext, + id: string, + data: MapUpdateIn['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< + MapAttributes, + MapAttributes + >(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 partialSavedObject = await soClient.update( + SO_TYPE, + id, + dataToLatest, + optionsToLatest + ); + + // Validate DB response and DOWN transform to the request version + const { value, error: resultError } = transforms.update.out.result.down< + MapUpdateOut, + MapUpdateOut + >({ + item: savedObjectToMapItem(partialSavedObject, 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: MapSearchOptions = {} + ): 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< + MapSearchOptions, + MapSearchOptions + >(options); + if (optionsError) { + throw Boom.badRequest(`Invalid payload. ${optionsError.message}`); + } + const { onlyTitle = false } = 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: CONTENT_ID, + search: query.text, + perPage: query.limit, + page: query.cursor ? +query.cursor : undefined, + defaultSearchOperator: 'AND', + searchFields: onlyTitle ? ['title'] : ['title^3', 'description'], + fields: ['description', 'title'], + 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< + MapSearchOut, + MapSearchOut + >({ + hits: response.saved_objects.map((so) => savedObjectToMapItem(so, false)), + pagination: { + total: response.total, + }, + }); + + if (resultError) { + throw Boom.badRequest(`Invalid response. ${resultError.message}`); + } + + return value; + } +} diff --git a/x-pack/plugins/maps/server/embeddable/embeddable_migrations.ts b/x-pack/plugins/maps/server/embeddable/embeddable_migrations.ts index 424b77a2dc3db..99d1578363345 100644 --- a/x-pack/plugins/maps/server/embeddable/embeddable_migrations.ts +++ b/x-pack/plugins/maps/server/embeddable/embeddable_migrations.ts @@ -6,7 +6,7 @@ */ import type { SerializableRecord } from '@kbn/utility-types'; -import { MapSavedObjectAttributes } from '../../common/map_saved_object_type'; +import type { MapAttributes } from '../../common/content_management'; import { moveAttribution } from '../../common/migrations/move_attribution'; import { migrateOtherCategoryColor } from '../../common/migrations/migrate_other_category_color'; import { setEmsTmsDefaultModes } from '../../common/migrations/set_ems_tms_default_modes'; @@ -25,7 +25,7 @@ export const embeddableMigrations = { try { return { ...state, - attributes: moveAttribution(state as { attributes: MapSavedObjectAttributes }), + attributes: moveAttribution(state as { attributes: MapAttributes }), } as SerializableRecord; } catch (e) { // Do not fail migration @@ -37,7 +37,7 @@ export const embeddableMigrations = { try { return { ...state, - attributes: setEmsTmsDefaultModes(state as { attributes: MapSavedObjectAttributes }), + attributes: setEmsTmsDefaultModes(state as { attributes: MapAttributes }), } as SerializableRecord; } catch (e) { // Do not fail migration @@ -47,7 +47,7 @@ export const embeddableMigrations = { }, '8.0.1': (state: SerializableRecord) => { try { - const { attributes } = extractReferences(state as { attributes: MapSavedObjectAttributes }); + const { attributes } = extractReferences(state as { attributes: MapAttributes }); return { ...state, attributes, @@ -62,7 +62,7 @@ export const embeddableMigrations = { try { return { ...state, - attributes: renameLayerTypes(state as { attributes: MapSavedObjectAttributes }), + attributes: renameLayerTypes(state as { attributes: MapAttributes }), } as SerializableRecord; } catch (e) { // Do not fail migration @@ -74,7 +74,7 @@ export const embeddableMigrations = { try { return { ...state, - attributes: migrateOtherCategoryColor(state as { attributes: MapSavedObjectAttributes }), + attributes: migrateOtherCategoryColor(state as { attributes: MapAttributes }), } as SerializableRecord; } catch (e) { // Do not fail migration diff --git a/x-pack/plugins/maps/server/maps_telemetry/find_maps.ts b/x-pack/plugins/maps/server/maps_telemetry/find_maps.ts index 213c1a6cde3ee..b2199663df658 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/find_maps.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/find_maps.ts @@ -8,16 +8,16 @@ import { asyncForEach } from '@kbn/std'; import type { ISavedObjectsRepository, SavedObject } from '@kbn/core/server'; import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; -import type { MapSavedObjectAttributes } from '../../common/map_saved_object_type'; +import type { MapAttributes } from '../../common/content_management'; export async function findMaps( savedObjectsClient: Pick, - callback: (savedObject: SavedObject) => Promise + callback: (savedObject: SavedObject) => Promise ) { let nextPage = 1; let hasMorePages = false; do { - const results = await savedObjectsClient.find({ + const results = await savedObjectsClient.find({ type: MAP_SAVED_OBJECT_TYPE, page: nextPage, }); diff --git a/x-pack/plugins/maps/server/maps_telemetry/map_stats/map_stats_collector.ts b/x-pack/plugins/maps/server/maps_telemetry/map_stats/map_stats_collector.ts index 4668dab8f464f..e300c2a584873 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/map_stats/map_stats_collector.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/map_stats/map_stats_collector.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { MapSavedObjectAttributes } from '../../../common/map_saved_object_type'; +import type { MapAttributes } from '../../../common/content_management'; import { EMS_BASEMAP_KEYS, JOIN_KEYS, @@ -38,7 +38,7 @@ export class MapStatsCollector { private _customIconsCountStats: ClusterCountStats | undefined; private _sourceCountStats: ClusterCountStats | undefined; - push(attributes: MapSavedObjectAttributes) { + push(attributes: MapAttributes) { if (!attributes || !attributes.mapStateJSON || !attributes.layerListJSON) { return; } diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 1dc497a87fdd9..8945b7d034377 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -17,6 +17,8 @@ import { import { HomeServerPluginSetup } from '@kbn/home-plugin/server'; import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; import type { EMSSettings } from '@kbn/maps-ems-plugin/server'; + +import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; import { getEcommerceSavedObjects } from './sample_data/ecommerce_saved_objects'; import { getFlightsSavedObjects } from './sample_data/flights_saved_objects'; import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects'; @@ -30,6 +32,7 @@ import { setupEmbeddable } from './embeddable'; import { setupSavedObjects } from './saved_objects'; import { registerIntegrations } from './register_integrations'; import { StartDeps, SetupDeps } from './types'; +import { MapsStorage } from './content_management'; export class MapsPlugin implements Plugin { readonly _initializerContext: PluginInitializerContext; @@ -150,7 +153,7 @@ export class MapsPlugin implements Plugin { DataViewPersistableStateService ); - const { usageCollection, home, features, customIntegrations } = plugins; + const { usageCollection, home, features, customIntegrations, contentManagement } = plugins; const config$ = this._initializerContext.config.create(); const emsSettings = plugins.mapsEms.createEMSSettings(); @@ -199,6 +202,14 @@ export class MapsPlugin implements Plugin { setupSavedObjects(core, getFilterMigrations, getDataViewMigrations); registerMapsUsageCollector(usageCollection); + contentManagement.register({ + id: CONTENT_ID, + storage: new MapsStorage(), + version: { + latest: LATEST_VERSION, + }, + }); + setupEmbeddable(plugins.embeddable, getFilterMigrations, getDataViewMigrations); return { diff --git a/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.ts b/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.ts index 4a55697e0f138..4c5fbc36412ad 100644 --- a/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.ts +++ b/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.ts @@ -21,12 +21,12 @@ import { addTypeToTermJoin } from '../../common/migrations/add_type_to_termjoin' import { moveAttribution } from '../../common/migrations/move_attribution'; import { setEmsTmsDefaultModes } from '../../common/migrations/set_ems_tms_default_modes'; import { renameLayerTypes } from '../../common/migrations/rename_layer_types'; -import type { MapSavedObjectAttributes } from '../../common/map_saved_object_type'; +import type { MapAttributes } from '../../common/content_management'; function logMigrationWarning( context: SavedObjectMigrationContext, errorMsg: string, - doc: SavedObjectUnsanitizedDoc + doc: SavedObjectUnsanitizedDoc ) { context.log.warn( `map migration failed (${context.migrationVersion}). ${errorMsg}. attributes: ${JSON.stringify( @@ -44,7 +44,7 @@ function logMigrationWarning( */ export const savedObjectMigrations = { '7.2.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -61,7 +61,7 @@ export const savedObjectMigrations = { } }, '7.4.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -77,7 +77,7 @@ export const savedObjectMigrations = { } }, '7.5.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -93,7 +93,7 @@ export const savedObjectMigrations = { } }, '7.6.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -110,7 +110,7 @@ export const savedObjectMigrations = { } }, '7.7.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -127,7 +127,7 @@ export const savedObjectMigrations = { } }, '7.8.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -143,7 +143,7 @@ export const savedObjectMigrations = { } }, '7.9.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -159,7 +159,7 @@ export const savedObjectMigrations = { } }, '7.10.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -175,7 +175,7 @@ export const savedObjectMigrations = { } }, '7.12.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -191,7 +191,7 @@ export const savedObjectMigrations = { } }, '7.14.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -207,7 +207,7 @@ export const savedObjectMigrations = { } }, '8.0.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -223,7 +223,7 @@ export const savedObjectMigrations = { } }, '8.1.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { @@ -239,7 +239,7 @@ export const savedObjectMigrations = { } }, '8.4.0': ( - doc: SavedObjectUnsanitizedDoc, + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext ) => { try { diff --git a/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.ts b/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.ts index 126ec9851c905..a8500968af3cf 100644 --- a/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.ts +++ b/x-pack/plugins/maps/server/saved_objects/setup_saved_objects.ts @@ -11,9 +11,10 @@ import type { SavedObjectMigrationMap } from '@kbn/core/server'; import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common'; import { mergeSavedObjectMigrationMaps } from '@kbn/core/server'; import { APP_ICON, getFullPath } from '../../common/constants'; +import { CONTENT_ID } from '../../common/content_management'; import { migrateDataPersistedState } from '../../common/migrations/migrate_data_persisted_state'; import { migrateDataViewsPersistedState } from '../../common/migrations/migrate_data_view_persisted_state'; -import type { MapSavedObjectAttributes } from '../../common/map_saved_object_type'; +import type { MapAttributes } from '../../common/content_management'; import { savedObjectMigrations } from './saved_object_migrations'; export function setupSavedObjects( @@ -21,8 +22,8 @@ export function setupSavedObjects( getFilterMigrations: () => MigrateFunctionsObject, getDataViewMigrations: () => MigrateFunctionsObject ) { - core.savedObjects.registerType({ - name: 'map', + core.savedObjects.registerType({ + name: CONTENT_ID, hidden: false, namespaceType: 'multiple-isolated', convertToMultiNamespaceTypeVersion: '8.0.0', @@ -71,7 +72,7 @@ export const getMapsFilterMigrations = ( ): MigrateFunctionsObject => mapValues( filterMigrations, - (filterMigration) => (doc: SavedObjectUnsanitizedDoc) => { + (filterMigration) => (doc: SavedObjectUnsanitizedDoc) => { try { const attributes = migrateDataPersistedState(doc, filterMigration); @@ -93,20 +94,17 @@ export const getMapsFilterMigrations = ( export const getMapsDataViewMigrations = ( migrations: MigrateFunctionsObject ): MigrateFunctionsObject => - mapValues( - migrations, - (migration) => (doc: SavedObjectUnsanitizedDoc) => { - try { - const attributes = migrateDataViewsPersistedState(doc, migration); + mapValues(migrations, (migration) => (doc: SavedObjectUnsanitizedDoc) => { + try { + const attributes = migrateDataViewsPersistedState(doc, migration); - return { - ...doc, - attributes, - }; - } catch (e) { - // Do not fail migration - // Maps application can display error when saved object is viewed - return doc; - } + return { + ...doc, + attributes, + }; + } catch (e) { + // Do not fail migration + // Maps application can display error when saved object is viewed + return doc; } - ); + }); diff --git a/x-pack/plugins/maps/server/types.ts b/x-pack/plugins/maps/server/types.ts index 912e9edb72e7d..4e9c6af769645 100644 --- a/x-pack/plugins/maps/server/types.ts +++ b/x-pack/plugins/maps/server/types.ts @@ -16,6 +16,7 @@ import { PluginStart as DataPluginStart, } from '@kbn/data-plugin/server'; import { CustomIntegrationsPluginSetup } from '@kbn/custom-integrations-plugin/server'; +import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server'; export interface SetupDeps { data: DataPluginSetup; @@ -26,6 +27,7 @@ export interface SetupDeps { mapsEms: MapsEmsPluginServerSetup; embeddable: EmbeddableSetup; customIntegrations: CustomIntegrationsPluginSetup; + contentManagement: ContentManagementServerSetup; } export interface StartDeps { diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index 00cb20bc9e8e8..9e501ba66559c 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -56,7 +56,6 @@ "@kbn/mapbox-gl", "@kbn/core-execution-context-common", "@kbn/chart-icons", - "@kbn/core-saved-objects-api-browser", "@kbn/ui-theme", "@kbn/monaco", "@kbn/safer-lodash-set", @@ -64,6 +63,9 @@ "@kbn/config-schema", "@kbn/controls-plugin", "@kbn/shared-ux-router", + "@kbn/content-management-plugin", + "@kbn/core-saved-objects-api-server", + "@kbn/object-versioning", "@kbn/field-types", ], "exclude": [