From da7676cf528d4c6e2e66335881acb010793e3c12 Mon Sep 17 00:00:00 2001 From: ppisljar Date: Wed, 14 Jun 2023 14:53:54 +0200 Subject: [PATCH] content management - event annotations --- .../common/content_management/cm_services.ts | 21 ++ .../common/content_management/constants.ts | 11 + .../common/content_management/index.ts | 32 ++ .../common/content_management/latest.ts | 9 + .../common/content_management/types.ts | 9 + .../content_management/v1/cm_services.ts | 141 ++++++++ .../common/content_management/v1/index.ts | 27 ++ .../common/content_management/v1/types.ts | 125 +++++++ src/plugins/event_annotation/common/index.ts | 1 + src/plugins/event_annotation/kibana.jsonc | 3 +- .../public/event_annotation_service/index.tsx | 10 +- .../event_annotation_service/service.test.ts | 95 +++-- .../event_annotation_service/service.tsx | 111 ++++-- src/plugins/event_annotation/public/mocks.ts | 9 + src/plugins/event_annotation/public/plugin.ts | 21 +- .../event_annotation_group_storage.ts | 324 ++++++++++++++++++ .../server/content_management/index.ts | 9 + src/plugins/event_annotation/server/plugin.ts | 12 + .../event_annotation/server/saved_objects.ts | 5 +- src/plugins/event_annotation/tsconfig.json | 5 + 20 files changed, 896 insertions(+), 84 deletions(-) create mode 100644 src/plugins/event_annotation/common/content_management/cm_services.ts create mode 100644 src/plugins/event_annotation/common/content_management/constants.ts create mode 100644 src/plugins/event_annotation/common/content_management/index.ts create mode 100644 src/plugins/event_annotation/common/content_management/latest.ts create mode 100644 src/plugins/event_annotation/common/content_management/types.ts create mode 100644 src/plugins/event_annotation/common/content_management/v1/cm_services.ts create mode 100644 src/plugins/event_annotation/common/content_management/v1/index.ts create mode 100644 src/plugins/event_annotation/common/content_management/v1/types.ts create mode 100644 src/plugins/event_annotation/server/content_management/event_annotation_group_storage.ts create mode 100644 src/plugins/event_annotation/server/content_management/index.ts diff --git a/src/plugins/event_annotation/common/content_management/cm_services.ts b/src/plugins/event_annotation/common/content_management/cm_services.ts new file mode 100644 index 00000000000000..fa050138b35ff4 --- /dev/null +++ b/src/plugins/event_annotation/common/content_management/cm_services.ts @@ -0,0 +1,21 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { + ContentManagementServicesDefinition as ServicesDefinition, + Version, +} from '@kbn/object-versioning'; + +// We export the versioned 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/src/plugins/event_annotation/common/content_management/constants.ts b/src/plugins/event_annotation/common/content_management/constants.ts new file mode 100644 index 00000000000000..45d44ab06145a4 --- /dev/null +++ b/src/plugins/event_annotation/common/content_management/constants.ts @@ -0,0 +1,11 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const LATEST_VERSION = 1; + +export const CONTENT_ID = 'event-annotation-group'; diff --git a/src/plugins/event_annotation/common/content_management/index.ts b/src/plugins/event_annotation/common/content_management/index.ts new file mode 100644 index 00000000000000..821ff93f903d3d --- /dev/null +++ b/src/plugins/event_annotation/common/content_management/index.ts @@ -0,0 +1,32 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { LATEST_VERSION, CONTENT_ID } from './constants'; + +export type { EventAnnotationGroupContentType } from './types'; + +export type { + EventAnnotationGroupSavedObject, + PartialEventAnnotationGroupSavedObject, + EventAnnotationGroupSavedObjectAttributes, + EventAnnotationGroupGetIn, + EventAnnotationGroupGetOut, + EventAnnotationGroupCreateIn, + EventAnnotationGroupCreateOut, + CreateOptions, + EventAnnotationGroupUpdateIn, + EventAnnotationGroupUpdateOut, + UpdateOptions, + EventAnnotationGroupDeleteIn, + EventAnnotationGroupDeleteOut, + EventAnnotationGroupSearchIn, + EventAnnotationGroupSearchOut, + EventAnnotationGroupSearchQuery, +} from './latest'; + +export * as EventAnnotationGroupV1 from './v1'; diff --git a/src/plugins/event_annotation/common/content_management/latest.ts b/src/plugins/event_annotation/common/content_management/latest.ts new file mode 100644 index 00000000000000..e9c79f0f50f936 --- /dev/null +++ b/src/plugins/event_annotation/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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './v1'; diff --git a/src/plugins/event_annotation/common/content_management/types.ts b/src/plugins/event_annotation/common/content_management/types.ts new file mode 100644 index 00000000000000..922a1977fff122 --- /dev/null +++ b/src/plugins/event_annotation/common/content_management/types.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type EventAnnotationGroupContentType = 'event-annotation-group'; diff --git a/src/plugins/event_annotation/common/content_management/v1/cm_services.ts b/src/plugins/event_annotation/common/content_management/v1/cm_services.ts new file mode 100644 index 00000000000000..44991124e472bc --- /dev/null +++ b/src/plugins/event_annotation/common/content_management/v1/cm_services.ts @@ -0,0 +1,141 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +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 eventAnnotationGroupAttributesSchema = schema.object( + { + title: schema.string(), + description: schema.maybe(schema.string()), + ignoreGlobalFilters: schema.boolean(), + annotations: schema.arrayOf(schema.any()), + dataViewSpec: schema.maybe(schema.any()), + }, + { unknowns: 'forbid' } +); + +const eventAnnotationGroupSavedObjectSchema = 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: eventAnnotationGroupAttributesSchema, + references: referencesSchema, + namespaces: schema.maybe(schema.arrayOf(schema.string())), + originId: schema.maybe(schema.string()), + }, + { unknowns: 'allow' } +); + +const getResultSchema = schema.object( + { + item: eventAnnotationGroupSavedObjectSchema, + 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({ + overwrite: schema.maybe(schema.boolean()), + 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: eventAnnotationGroupAttributesSchema, + }, + }, + out: { + result: { + schema: schema.object( + { + item: eventAnnotationGroupSavedObjectSchema, + }, + { unknowns: 'forbid' } + ), + }, + }, + }, + update: { + in: { + options: { + schema: createOptionsSchema, // same schema as "create" + }, + data: { + schema: eventAnnotationGroupAttributesSchema, + }, + }, + }, + search: { + in: { + options: { + schema: schema.maybe( + schema.object( + { + searchFields: schema.maybe(schema.arrayOf(schema.string())), + types: schema.maybe(schema.arrayOf(schema.string())), + }, + { unknowns: 'forbid' } + ) + ), + }, + }, + }, +}; diff --git a/src/plugins/event_annotation/common/content_management/v1/index.ts b/src/plugins/event_annotation/common/content_management/v1/index.ts new file mode 100644 index 00000000000000..d05d743a199a85 --- /dev/null +++ b/src/plugins/event_annotation/common/content_management/v1/index.ts @@ -0,0 +1,27 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { + EventAnnotationGroupSavedObject as EventAnnotationGroupSavedObject, + PartialEventAnnotationGroupSavedObject, + EventAnnotationGroupSavedObjectAttributes, + EventAnnotationGroupGetIn, + EventAnnotationGroupGetOut, + EventAnnotationGroupCreateIn, + EventAnnotationGroupCreateOut, + CreateOptions, + EventAnnotationGroupUpdateIn, + EventAnnotationGroupUpdateOut, + UpdateOptions, + EventAnnotationGroupDeleteIn, + EventAnnotationGroupDeleteOut, + EventAnnotationGroupSearchIn, + EventAnnotationGroupSearchOut, + EventAnnotationGroupSearchQuery, + Reference, +} from './types'; diff --git a/src/plugins/event_annotation/common/content_management/v1/types.ts b/src/plugins/event_annotation/common/content_management/v1/types.ts new file mode 100644 index 00000000000000..a5c23061008215 --- /dev/null +++ b/src/plugins/event_annotation/common/content_management/v1/types.ts @@ -0,0 +1,125 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + GetIn, + CreateIn, + SearchIn, + UpdateIn, + DeleteIn, + DeleteResult, + SearchResult, + GetResult, + CreateResult, + UpdateResult, +} from '@kbn/content-management-plugin/common'; + +import type { DataViewSpec } from '@kbn/data-views-plugin/common'; +import { EventAnnotationGroupContentType } from '../types'; +import { EventAnnotationConfig } from '../../types'; + +export interface Reference { + type: string; + id: string; + name: string; +} + +export interface EventAnnotationGroupSavedObjectAttributes { + title: string; + description: string; + ignoreGlobalFilters: boolean; + annotations: EventAnnotationConfig[]; + dataViewSpec?: DataViewSpec; +} + +export interface EventAnnotationGroupSavedObject { + id: string; + type: string; + version?: string; + updatedAt?: string; + createdAt?: string; + attributes: EventAnnotationGroupSavedObjectAttributes; + references: Reference[]; + namespaces?: string[]; + originId?: string; + error?: { + error: string; + message: string; + statusCode: number; + metadata?: Record; + }; +} + +export type PartialEventAnnotationGroupSavedObject = Omit< + EventAnnotationGroupSavedObject, + 'attributes' | 'references' +> & { + attributes: Partial; + references: Reference[] | undefined; +}; +// ----------- GET -------------- + +export type EventAnnotationGroupGetIn = GetIn; + +export type EventAnnotationGroupGetOut = GetResult< + EventAnnotationGroupSavedObject, + { + outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; + aliasTargetId?: string; + aliasPurpose?: 'savedObjectConversion' | 'savedObjectImport'; + } +>; + +// ----------- 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 EventAnnotationGroupCreateIn = CreateIn< + EventAnnotationGroupContentType, + EventAnnotationGroupSavedObjectAttributes, + CreateOptions +>; + +export type EventAnnotationGroupCreateOut = CreateResult; + +// ----------- UPDATE -------------- + +export interface UpdateOptions { + /** Array of referenced saved objects. */ + references?: Reference[]; +} + +export type EventAnnotationGroupUpdateIn = UpdateIn< + EventAnnotationGroupContentType, + EventAnnotationGroupSavedObjectAttributes, + UpdateOptions +>; + +export type EventAnnotationGroupUpdateOut = UpdateResult; + +// ----------- DELETE -------------- + +export type EventAnnotationGroupDeleteIn = DeleteIn; + +export type EventAnnotationGroupDeleteOut = DeleteResult; + +// ----------- SEARCH -------------- + +export interface EventAnnotationGroupSearchQuery { + types?: string[]; + searchFields?: string[]; +} + +export type EventAnnotationGroupSearchIn = SearchIn; + +export type EventAnnotationGroupSearchOut = SearchResult; diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index 0341a9e5ed4a25..e0d773b4c996ea 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -44,4 +44,5 @@ export type { EventAnnotationGroupAttributes, } from './types'; +export type { EventAnnotationGroupSavedObjectAttributes } from './content_management'; export { EVENT_ANNOTATION_GROUP_TYPE, ANNOTATIONS_LISTING_VIEW_ID } from './constants'; diff --git a/src/plugins/event_annotation/kibana.jsonc b/src/plugins/event_annotation/kibana.jsonc index 1099c467d502f6..17f62e739e6c4e 100644 --- a/src/plugins/event_annotation/kibana.jsonc +++ b/src/plugins/event_annotation/kibana.jsonc @@ -16,7 +16,8 @@ "dataViews", "unifiedSearch", "kibanaUtils", - "visualizationUiComponents" + "visualizationUiComponents", + "contentManagement" ], "optionalPlugins": [ "savedObjectsTagging", diff --git a/src/plugins/event_annotation/public/event_annotation_service/index.tsx b/src/plugins/event_annotation/public/event_annotation_service/index.tsx index 18ef89681d6213..5e509ac8572183 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/index.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/index.tsx @@ -8,6 +8,7 @@ import { CoreStart } from '@kbn/core/public'; import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import { EventAnnotationServiceType } from './types'; export class EventAnnotationService { @@ -15,9 +16,15 @@ export class EventAnnotationService { private core: CoreStart; private savedObjectsManagement: SavedObjectsManagementPluginStart; + private contentManagement: ContentManagementPublicStart; - constructor(core: CoreStart, savedObjectsManagement: SavedObjectsManagementPluginStart) { + constructor( + core: CoreStart, + contentManagement: ContentManagementPublicStart, + savedObjectsManagement: SavedObjectsManagementPluginStart + ) { this.core = core; + this.contentManagement = contentManagement; this.savedObjectsManagement = savedObjectsManagement; } @@ -26,6 +33,7 @@ export class EventAnnotationService { const { getEventAnnotationService } = await import('./service'); this.eventAnnotationService = getEventAnnotationService( this.core, + this.contentManagement, this.savedObjectsManagement ); } diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts index 905435bc4f4d06..b446e454636b4a 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { SavedObjectsFindResponse } from '@kbn/core-saved-objects-api-browser'; import { CoreStart, SimpleSavedObject } from '@kbn/core/public'; +import { ContentClient, ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import { coreMock } from '@kbn/core/public/mocks'; import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import { EventAnnotationConfig, EventAnnotationGroupAttributes } from '../../common'; @@ -131,28 +131,35 @@ const annotationResolveMocks = { }, }; +const contentClient = { + get: jest.fn(), + search: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), +} as unknown as ContentClient; + let core: CoreStart; describe('Event Annotation Service', () => { let eventAnnotationService: EventAnnotationServiceType; beforeEach(() => { core = coreMock.createStart(); - (core.savedObjects.client.create as jest.Mock).mockImplementation(() => { - return annotationGroupResolveMocks.multiAnnotations; + (contentClient.create as jest.Mock).mockImplementation(() => { + return { item: annotationGroupResolveMocks.multiAnnotations }; }); - (core.savedObjects.client.get as jest.Mock).mockImplementation((_type, id) => { + (contentClient.get as jest.Mock).mockImplementation(({ contentTypeId, id }) => { const typedId = id as keyof typeof annotationGroupResolveMocks; - return annotationGroupResolveMocks[typedId]; + return { item: annotationGroupResolveMocks[typedId] }; }); - (core.savedObjects.client.find as jest.Mock).mockResolvedValue({ - total: 10, - savedObjects: Object.values(annotationGroupResolveMocks), - } as Pick, 'total' | 'savedObjects'>); - (core.savedObjects.client.bulkCreate as jest.Mock).mockImplementation(() => { - return annotationResolveMocks.multiAnnotations; + (contentClient.search as jest.Mock).mockResolvedValue({ + pagination: { total: 10 }, + hits: Object.values(annotationGroupResolveMocks), }); + (contentClient.delete as jest.Mock).mockResolvedValue({}); eventAnnotationService = getEventAnnotationService( core, + { client: contentClient } as ContentManagementPublicStart, {} as SavedObjectsManagementPluginStart ); }); @@ -512,28 +519,14 @@ describe('Event Annotation Service', () => { expect(content).toMatchSnapshot(); - expect((core.savedObjects.client.find as jest.Mock).mock.calls).toMatchInlineSnapshot(` + expect((contentClient.search as jest.Mock).mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { - "defaultSearchOperator": "AND", - "hasNoReference": undefined, - "hasReference": Array [ - Object { - "id": "1234", - "type": "mytype", - }, - ], - "page": 1, - "perPage": 20, - "search": "my search*", - "searchFields": Array [ - "title^3", - "description", - ], - "type": Array [ - "event-annotation-group", - ], + "contentTypeId": "event-annotation-group", + "query": Object { + "text": "my search*", + }, }, ], ] @@ -543,10 +536,14 @@ describe('Event Annotation Service', () => { describe('deleteAnnotationGroups', () => { it('deletes annotation group along with annotations that reference them', async () => { await eventAnnotationService.deleteAnnotationGroups(['id1', 'id2']); - expect(core.savedObjects.client.bulkDelete).toHaveBeenCalledWith([ - { id: 'id1', type: 'event-annotation-group' }, - { id: 'id2', type: 'event-annotation-group' }, - ]); + expect(contentClient.delete).toHaveBeenCalledWith({ + id: 'id1', + contentTypeId: 'event-annotation-group', + }); + expect(contentClient.delete).toHaveBeenCalledWith({ + id: 'id2', + contentTypeId: 'event-annotation-group', + }); }); }); describe('createAnnotationGroup', () => { @@ -563,16 +560,16 @@ describe('Event Annotation Service', () => { ignoreGlobalFilters: false, annotations, }); - expect(core.savedObjects.client.create).toHaveBeenCalledWith( - 'event-annotation-group', - { + expect(contentClient.create).toHaveBeenCalledWith({ + contentTypeId: 'event-annotation-group', + data: { title: 'newGroupTitle', description: 'my description', ignoreGlobalFilters: false, - dataViewSpec: null, + dataViewSpec: undefined, annotations, }, - { + options: { references: [ { id: 'ipid', @@ -595,8 +592,8 @@ describe('Event Annotation Service', () => { type: 'tag', }, ], - } - ); + }, + }); }); }); describe('updateAnnotationGroup', () => { @@ -612,17 +609,17 @@ describe('Event Annotation Service', () => { }, 'multiAnnotations' ); - expect(core.savedObjects.client.update).toHaveBeenCalledWith( - 'event-annotation-group', - 'multiAnnotations', - { + expect(contentClient.update).toHaveBeenCalledWith({ + contentTypeId: 'event-annotation-group', + id: 'multiAnnotations', + data: { title: 'newTitle', description: '', annotations: [], - dataViewSpec: null, + dataViewSpec: undefined, ignoreGlobalFilters: false, } as EventAnnotationGroupAttributes, - { + options: { references: [ { id: 'newId', @@ -630,8 +627,8 @@ describe('Event Annotation Service', () => { type: 'index-pattern', }, ], - } - ); + }, + }); }); }); }); diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index 65c2b9146df1cd..724b1093145fcc 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -13,18 +13,16 @@ import { ExpressionAstExpression } from '@kbn/expressions-plugin/common'; import { CoreStart, SavedObjectReference, - SavedObjectsClientContract, SavedObjectsFindOptions, SavedObjectsFindOptionsReference, - SimpleSavedObject, } from '@kbn/core/public'; import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import { defaultAnnotationLabel } from '../../common/manual_event_annotation'; import { EventAnnotationGroupContent } from '../../common/types'; import { EventAnnotationConfig, - EventAnnotationGroupAttributes, EventAnnotationGroupConfig, EVENT_ANNOTATION_GROUP_TYPE, } from '../../common'; @@ -36,6 +34,20 @@ import { isQueryAnnotationConfig, } from './helpers'; import { EventAnnotationGroupSavedObjectFinder } from '../components/event_annotation_group_saved_object_finder'; +import { + EventAnnotationGroupCreateIn, + EventAnnotationGroupCreateOut, + EventAnnotationGroupDeleteIn, + EventAnnotationGroupDeleteOut, + EventAnnotationGroupGetIn, + EventAnnotationGroupGetOut, + EventAnnotationGroupSavedObject, + EventAnnotationGroupSavedObjectAttributes, + EventAnnotationGroupSearchIn, + EventAnnotationGroupSearchOut, + EventAnnotationGroupUpdateIn, + EventAnnotationGroupUpdateOut, +} from '../../common/content_management'; export function hasIcon(icon: string | undefined): icon is string { return icon != null && icon !== 'empty'; @@ -43,12 +55,13 @@ export function hasIcon(icon: string | undefined): icon is string { export function getEventAnnotationService( core: CoreStart, + contentManagement: ContentManagementPublicStart, savedObjectsManagement: SavedObjectsManagementPluginStart ): EventAnnotationServiceType { - const client: SavedObjectsClientContract = core.savedObjects.client; + const client = contentManagement.client; const mapSavedObjectToGroupConfig = ( - savedObject: SimpleSavedObject + savedObject: EventAnnotationGroupSavedObject ): EventAnnotationGroupConfig => { const adHocDataViewSpec = savedObject.attributes.dataViewSpec ? DataViewPersistableStateService.inject( @@ -71,7 +84,7 @@ export function getEventAnnotationService( }; const mapSavedObjectToGroupContent = ( - savedObject: SimpleSavedObject + savedObject: EventAnnotationGroupSavedObject ): EventAnnotationGroupContent => { const groupConfig = mapSavedObjectToGroupConfig(savedObject); @@ -92,16 +105,16 @@ export function getEventAnnotationService( const loadAnnotationGroup = async ( savedObjectId: string ): Promise => { - const savedObject = await client.get( - EVENT_ANNOTATION_GROUP_TYPE, - savedObjectId - ); + const savedObject = await client.get({ + contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + id: savedObjectId, + }); - if (savedObject.error) { - throw savedObject.error; + if (savedObject.item.error) { + throw savedObject.item.error; } - return mapSavedObjectToGroupConfig(savedObject); + return mapSavedObjectToGroupConfig(savedObject.item); }; const findAnnotationGroupContent = async ( @@ -121,18 +134,29 @@ export function getEventAnnotationService( hasNoReference: referencesToExclude, }; - const { total, savedObjects } = await client.find( - searchOptions - ); + const { pagination, hits } = await client.search< + EventAnnotationGroupSearchIn, + EventAnnotationGroupSearchOut + >({ + contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + query: { + text: searchOptions.search, + }, + }); return { - total, - hits: savedObjects.map(mapSavedObjectToGroupContent), + total: pagination.total, + hits: hits.map(mapSavedObjectToGroupContent), }; }; const deleteAnnotationGroups = async (ids: string[]): Promise => { - await client.bulkDelete([...ids.map((id) => ({ type: EVENT_ANNOTATION_GROUP_TYPE, id }))]); + for (const id of ids) { + await client.delete({ + contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + id, + }); + } }; const extractDataViewInformation = (group: EventAnnotationGroupConfig) => { @@ -165,7 +189,10 @@ export function getEventAnnotationService( const getAnnotationGroupAttributesAndReferences = ( group: EventAnnotationGroupConfig - ): { attributes: EventAnnotationGroupAttributes; references: SavedObjectReference[] } => { + ): { + attributes: EventAnnotationGroupSavedObjectAttributes; + references: SavedObjectReference[]; + } => { const { references, dataViewSpec } = extractDataViewInformation(group); const { title, description, tags, ignoreGlobalFilters, annotations } = group; @@ -178,7 +205,13 @@ export function getEventAnnotationService( ); return { - attributes: { title, description, ignoreGlobalFilters, annotations, dataViewSpec }, + attributes: { + title, + description, + ignoreGlobalFilters, + annotations, + dataViewSpec: dataViewSpec || undefined, + }, references, }; }; @@ -189,10 +222,16 @@ export function getEventAnnotationService( const { attributes, references } = getAnnotationGroupAttributesAndReferences(group); const groupSavedObjectId = ( - await client.create(EVENT_ANNOTATION_GROUP_TYPE, attributes, { - references, + await client.create({ + contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + data: { + ...attributes, + }, + options: { + references, + }, }) - ).id; + ).item.id; return { id: groupSavedObjectId }; }; @@ -203,18 +242,30 @@ export function getEventAnnotationService( ): Promise => { const { attributes, references } = getAnnotationGroupAttributesAndReferences(group); - await client.update(EVENT_ANNOTATION_GROUP_TYPE, annotationGroupId, attributes, { - references, + await client.update({ + contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + id: annotationGroupId, + data: { + ...attributes, + }, + options: { + references, + }, }); }; const checkHasAnnotationGroups = async (): Promise => { - const response = await client.find({ - type: EVENT_ANNOTATION_GROUP_TYPE, - perPage: 0, + const response = await client.search< + EventAnnotationGroupSearchIn, + EventAnnotationGroupSearchOut + >({ + contentTypeId: EVENT_ANNOTATION_GROUP_TYPE, + query: { + text: '*', + }, }); - return response.total > 0; + return response.pagination.total > 0; }; return { diff --git a/src/plugins/event_annotation/public/mocks.ts b/src/plugins/event_annotation/public/mocks.ts index 100b5d3f1c3e2e..f0e4dc34b876c5 100644 --- a/src/plugins/event_annotation/public/mocks.ts +++ b/src/plugins/event_annotation/public/mocks.ts @@ -8,10 +8,19 @@ import { coreMock } from '@kbn/core/public/mocks'; import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; +import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import { getEventAnnotationService } from './event_annotation_service/service'; // not really mocking but avoiding async loading export const eventAnnotationServiceMock = getEventAnnotationService( coreMock.createStart(), + { + client: { + get: jest.fn(), + search: jest.fn(), + create: jest.fn(), + update: jest.fn(), + }, + } as unknown as ContentManagementPublicStart, {} as SavedObjectsManagementPluginStart ); diff --git a/src/plugins/event_annotation/public/plugin.ts b/src/plugins/event_annotation/public/plugin.ts index 576f8a3b2a8f04..fd8a8fb885891f 100644 --- a/src/plugins/event_annotation/public/plugin.ts +++ b/src/plugins/event_annotation/public/plugin.ts @@ -12,6 +12,7 @@ import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-p import type { ExpressionsSetup } from '@kbn/expressions-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; +import {ContentManagementPublicSetup, ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public/types'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { VisualizationsSetup } from '@kbn/visualizations-plugin/public'; @@ -27,6 +28,7 @@ import { import { getFetchEventAnnotations } from './fetch_event_annotations'; import type { EventAnnotationListingPageServices } from './get_table_list'; import { ANNOTATIONS_LISTING_VIEW_ID } from '../common/constants'; +import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; export interface EventAnnotationStartDependencies { savedObjectsManagement: SavedObjectsManagementPluginStart; @@ -35,11 +37,13 @@ export interface EventAnnotationStartDependencies { presentationUtil: PresentationUtilPluginStart; dataViews: DataViewsPublicPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; + contentManagement: ContentManagementPublicStart; } interface SetupDependencies { expressions: ExpressionsSetup; visualizations: VisualizationsSetup; + contentManagement: ContentManagementPublicSetup; } /** @public */ @@ -62,6 +66,16 @@ export class EventAnnotationPlugin getFetchEventAnnotations({ getStartServices: core.getStartServices }) ); + dependencies.contentManagement.registry.register({ + id: CONTENT_ID, + version: { + latest: LATEST_VERSION, + }, + name: i18n.translate('eventAnnotation.content.name', { + defaultMessage: 'Annotation group', + }), + }); + dependencies.visualizations.listingViewRegistry.add({ title: i18n.translate('eventAnnotation.listingViewTitle', { defaultMessage: 'Annotation groups', @@ -72,6 +86,7 @@ export class EventAnnotationPlugin const eventAnnotationService = await new EventAnnotationService( coreStart, + pluginsStart.contentManagement, pluginsStart.savedObjectsManagement ).getService(); @@ -107,6 +122,10 @@ export class EventAnnotationPlugin core: CoreStart, startDependencies: EventAnnotationStartDependencies ): EventAnnotationService { - return new EventAnnotationService(core, startDependencies.savedObjectsManagement); + return new EventAnnotationService( + core, + startDependencies.contentManagement, + startDependencies.savedObjectsManagement + ); } } diff --git a/src/plugins/event_annotation/server/content_management/event_annotation_group_storage.ts b/src/plugins/event_annotation/server/content_management/event_annotation_group_storage.ts new file mode 100644 index 00000000000000..ce032374e7b535 --- /dev/null +++ b/src/plugins/event_annotation/server/content_management/event_annotation_group_storage.ts @@ -0,0 +1,324 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +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 { EVENT_ANNOTATION_GROUP_TYPE } from '../../common'; +import { cmServicesDefinition } from '../../common/content_management/cm_services'; +import type { + EventAnnotationGroupSavedObjectAttributes, + EventAnnotationGroupSavedObject, + PartialEventAnnotationGroupSavedObject, + EventAnnotationGroupGetOut, + EventAnnotationGroupCreateIn, + EventAnnotationGroupCreateOut, + CreateOptions, + EventAnnotationGroupUpdateIn, + EventAnnotationGroupUpdateOut, + UpdateOptions, + EventAnnotationGroupDeleteOut, + EventAnnotationGroupSearchQuery, + EventAnnotationGroupSearchOut, +} 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 savedObjectToEventAnnotationGroupSavedObject( + savedObject: SavedObject, + partial: false +): EventAnnotationGroupSavedObject; + +function savedObjectToEventAnnotationGroupSavedObject( + savedObject: PartialSavedObject, + partial: true +): PartialEventAnnotationGroupSavedObject; + +function savedObjectToEventAnnotationGroupSavedObject( + savedObject: + | SavedObject + | PartialSavedObject +): EventAnnotationGroupSavedObject | PartialEventAnnotationGroupSavedObject { + const { + id, + type, + updated_at: updatedAt, + created_at: createdAt, + attributes: { title, description, annotations, ignoreGlobalFilters, dataViewSpec }, + references, + error, + namespaces, + } = savedObject; + + return { + id, + type, + updatedAt, + createdAt, + attributes: { + title, + description, + annotations, + ignoreGlobalFilters, + dataViewSpec, + }, + references, + error, + namespaces, + }; +} + +const SO_TYPE = EVENT_ANNOTATION_GROUP_TYPE; + +export class EventAnnotationGroupStorage + 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: EventAnnotationGroupGetOut = { + item: savedObjectToEventAnnotationGroupSavedObject(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< + EventAnnotationGroupGetOut, + EventAnnotationGroupGetOut + >(response); + + if (resultError) { + throw Boom.badRequest(`Invalid response. ${resultError.message}`); + } + + return value; + } + + async bulkGet(): Promise { + // Not implemented. EventAnnotationGroup does not use bulkGet + throw new Error(`[bulkGet] has not been implemented. See EventAnnotationGroupStorage class.`); + } + + async create( + ctx: StorageContext, + data: EventAnnotationGroupCreateIn['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< + EventAnnotationGroupSavedObjectAttributes, + EventAnnotationGroupSavedObjectAttributes + >(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< + EventAnnotationGroupCreateOut, + EventAnnotationGroupCreateOut + >({ + item: savedObjectToEventAnnotationGroupSavedObject(savedObject, false), + }); + + if (resultError) { + throw Boom.badRequest(`Invalid response. ${resultError.message}`); + } + + return value; + } + + async update( + ctx: StorageContext, + id: string, + data: EventAnnotationGroupUpdateIn['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< + EventAnnotationGroupSavedObjectAttributes, + EventAnnotationGroupSavedObjectAttributes + >(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< + EventAnnotationGroupUpdateOut, + EventAnnotationGroupUpdateOut + >({ + item: savedObjectToEventAnnotationGroupSavedObject(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: EventAnnotationGroupSearchQuery = {} + ): 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< + EventAnnotationGroupSearchQuery, + EventAnnotationGroupSearchQuery + >(options); + if (optionsError) { + throw Boom.badRequest(`Invalid payload. ${optionsError.message}`); + } + const { searchFields = ['title^3', 'description'], types = [SO_TYPE] } = 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< + EventAnnotationGroupSearchOut, + EventAnnotationGroupSearchOut + >({ + hits: response.saved_objects.map((so) => + savedObjectToEventAnnotationGroupSavedObject(so, false) + ), + pagination: { + total: response.total, + }, + }); + + if (resultError) { + throw Boom.badRequest(`Invalid response. ${resultError.message}`); + } + + return value; + } +} diff --git a/src/plugins/event_annotation/server/content_management/index.ts b/src/plugins/event_annotation/server/content_management/index.ts new file mode 100644 index 00000000000000..6d896aa292dfa2 --- /dev/null +++ b/src/plugins/event_annotation/server/content_management/index.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { EventAnnotationGroupStorage } from './event_annotation_group_storage'; diff --git a/src/plugins/event_annotation/server/plugin.ts b/src/plugins/event_annotation/server/plugin.ts index d5e2fee4332309..8cd24f8938466a 100644 --- a/src/plugins/event_annotation/server/plugin.ts +++ b/src/plugins/event_annotation/server/plugin.ts @@ -9,6 +9,7 @@ import { CoreSetup, Plugin } from '@kbn/core/server'; import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server'; import { PluginStart as DataPluginStart } from '@kbn/data-plugin/server'; +import { ContentManagementServerSetup } from '@kbn/content-management-plugin/server'; import { manualPointEventAnnotation, eventAnnotationGroup, @@ -16,9 +17,12 @@ import { queryPointEventAnnotation, } from '../common'; import { setupSavedObjects } from './saved_objects'; +import { EventAnnotationGroupStorage } from './content_management'; +import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; interface SetupDependencies { expressions: ExpressionsServerSetup; + contentManagement: ContentManagementServerSetup; } export interface EventAnnotationStartDependencies { data: DataPluginStart; @@ -36,6 +40,14 @@ export class EventAnnotationServerPlugin implements Plugin { setupSavedObjects(core); + dependencies.contentManagement.register({ + id: CONTENT_ID, + storage: new EventAnnotationGroupStorage(), + version: { + latest: LATEST_VERSION, + }, + }); + return {}; } diff --git a/src/plugins/event_annotation/server/saved_objects.ts b/src/plugins/event_annotation/server/saved_objects.ts index ef357aae0c5460..f5b23b0bdd598d 100644 --- a/src/plugins/event_annotation/server/saved_objects.ts +++ b/src/plugins/event_annotation/server/saved_objects.ts @@ -16,7 +16,7 @@ import { import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; import { VISUALIZE_APP_NAME } from '@kbn/visualizations-plugin/common/constants'; import { ANNOTATIONS_LISTING_VIEW_ID, EVENT_ANNOTATION_GROUP_TYPE } from '../common/constants'; -import { EventAnnotationGroupAttributes } from '../common/types'; +import { EventAnnotationGroupSavedObjectAttributes } from '../common'; export function setupSavedObjects(coreSetup: CoreSetup) { coreSetup.savedObjects.registerType({ @@ -28,7 +28,8 @@ export function setupSavedObjects(coreSetup: CoreSetup) { icon: 'flag', defaultSearchField: 'title', importableAndExportable: true, - getTitle: (obj: { attributes: EventAnnotationGroupAttributes }) => obj.attributes.title, + getTitle: (obj: { attributes: EventAnnotationGroupSavedObjectAttributes }) => + obj.attributes.title, getInAppUrl: (obj: { id: string }) => ({ // TODO link to specific object path: `/app/${VISUALIZE_APP_NAME}#/${ANNOTATIONS_LISTING_VIEW_ID}`, diff --git a/src/plugins/event_annotation/tsconfig.json b/src/plugins/event_annotation/tsconfig.json index d8d9d61af2ac3b..fe28ccf07262a3 100644 --- a/src/plugins/event_annotation/tsconfig.json +++ b/src/plugins/event_annotation/tsconfig.json @@ -44,6 +44,11 @@ "@kbn/content-management-tabbed-table-list-view", "@kbn/core-notifications-browser", "@kbn/core-notifications-browser-mocks", + "@kbn/core-saved-objects-server", + "@kbn/object-versioning", + "@kbn/config-schema", + "@kbn/content-management-plugin", + "@kbn/core-saved-objects-api-server" ], "exclude": [ "target/**/*",