content management - event annotations (#159692)

This commit is contained in:
Peter Pisljar 2023-06-15 15:31:34 +02:00 committed by GitHub
parent 5751f29f58
commit b18d8d4c43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 899 additions and 84 deletions

View file

@ -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,
};

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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' }
)
),
},
},
},
};

View file

@ -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';

View file

@ -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<string, unknown>;
};
}
export type PartialEventAnnotationGroupSavedObject = Omit<
EventAnnotationGroupSavedObject,
'attributes' | 'references'
> & {
attributes: Partial<EventAnnotationGroupSavedObjectAttributes>;
references: Reference[] | undefined;
};
// ----------- GET --------------
export type EventAnnotationGroupGetIn = GetIn<EventAnnotationGroupContentType>;
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<EventAnnotationGroupSavedObject>;
// ----------- UPDATE --------------
export interface UpdateOptions {
/** Array of referenced saved objects. */
references?: Reference[];
}
export type EventAnnotationGroupUpdateIn = UpdateIn<
EventAnnotationGroupContentType,
EventAnnotationGroupSavedObjectAttributes,
UpdateOptions
>;
export type EventAnnotationGroupUpdateOut = UpdateResult<PartialEventAnnotationGroupSavedObject>;
// ----------- DELETE --------------
export type EventAnnotationGroupDeleteIn = DeleteIn<EventAnnotationGroupContentType>;
export type EventAnnotationGroupDeleteOut = DeleteResult;
// ----------- SEARCH --------------
export interface EventAnnotationGroupSearchQuery {
types?: string[];
searchFields?: string[];
}
export type EventAnnotationGroupSearchIn = SearchIn<EventAnnotationGroupContentType, {}>;
export type EventAnnotationGroupSearchOut = SearchResult<EventAnnotationGroupSavedObject>;

View file

@ -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';

View file

@ -16,7 +16,8 @@
"dataViews",
"unifiedSearch",
"kibanaUtils",
"visualizationUiComponents"
"visualizationUiComponents",
"contentManagement"
],
"optionalPlugins": [
"savedObjectsTagging",

View file

@ -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
);
}

View file

@ -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<SavedObjectsFindResponse<EventAnnotationGroupAttributes>, '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',
},
],
}
);
},
});
});
});
});

View file

@ -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<EventAnnotationGroupAttributes>
savedObject: EventAnnotationGroupSavedObject
): EventAnnotationGroupConfig => {
const adHocDataViewSpec = savedObject.attributes.dataViewSpec
? DataViewPersistableStateService.inject(
@ -71,7 +84,7 @@ export function getEventAnnotationService(
};
const mapSavedObjectToGroupContent = (
savedObject: SimpleSavedObject<EventAnnotationGroupAttributes>
savedObject: EventAnnotationGroupSavedObject
): EventAnnotationGroupContent => {
const groupConfig = mapSavedObjectToGroupConfig(savedObject);
@ -92,16 +105,16 @@ export function getEventAnnotationService(
const loadAnnotationGroup = async (
savedObjectId: string
): Promise<EventAnnotationGroupConfig> => {
const savedObject = await client.get<EventAnnotationGroupAttributes>(
EVENT_ANNOTATION_GROUP_TYPE,
savedObjectId
);
const savedObject = await client.get<EventAnnotationGroupGetIn, EventAnnotationGroupGetOut>({
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<EventAnnotationGroupAttributes>(
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<void> => {
await client.bulkDelete([...ids.map((id) => ({ type: EVENT_ANNOTATION_GROUP_TYPE, id }))]);
for (const id of ids) {
await client.delete<EventAnnotationGroupDeleteIn, EventAnnotationGroupDeleteOut>({
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<EventAnnotationGroupCreateIn, EventAnnotationGroupCreateOut>({
contentTypeId: EVENT_ANNOTATION_GROUP_TYPE,
data: {
...attributes,
},
options: {
references,
},
})
).id;
).item.id;
return { id: groupSavedObjectId };
};
@ -203,18 +242,30 @@ export function getEventAnnotationService(
): Promise<void> => {
const { attributes, references } = getAnnotationGroupAttributesAndReferences(group);
await client.update(EVENT_ANNOTATION_GROUP_TYPE, annotationGroupId, attributes, {
references,
await client.update<EventAnnotationGroupUpdateIn, EventAnnotationGroupUpdateOut>({
contentTypeId: EVENT_ANNOTATION_GROUP_TYPE,
id: annotationGroupId,
data: {
...attributes,
},
options: {
references,
},
});
};
const checkHasAnnotationGroups = async (): Promise<boolean> => {
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 {

View file

@ -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
);

View file

@ -12,6 +12,10 @@ 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 +31,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 +40,13 @@ export interface EventAnnotationStartDependencies {
presentationUtil: PresentationUtilPluginStart;
dataViews: DataViewsPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
contentManagement: ContentManagementPublicStart;
}
interface SetupDependencies {
expressions: ExpressionsSetup;
visualizations: VisualizationsSetup;
contentManagement: ContentManagementPublicSetup;
}
/** @public */
@ -62,6 +69,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 +89,7 @@ export class EventAnnotationPlugin
const eventAnnotationService = await new EventAnnotationService(
coreStart,
pluginsStart.contentManagement,
pluginsStart.savedObjectsManagement
).getService();
@ -107,6 +125,10 @@ export class EventAnnotationPlugin
core: CoreStart,
startDependencies: EventAnnotationStartDependencies
): EventAnnotationService {
return new EventAnnotationService(core, startDependencies.savedObjectsManagement);
return new EventAnnotationService(
core,
startDependencies.contentManagement,
startDependencies.savedObjectsManagement
);
}
}

View file

@ -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<T> = Omit<SavedObject<Partial<T>>, 'references'> & {
references: SavedObjectReference[] | undefined;
};
function savedObjectToEventAnnotationGroupSavedObject(
savedObject: SavedObject<EventAnnotationGroupSavedObjectAttributes>,
partial: false
): EventAnnotationGroupSavedObject;
function savedObjectToEventAnnotationGroupSavedObject(
savedObject: PartialSavedObject<EventAnnotationGroupSavedObjectAttributes>,
partial: true
): PartialEventAnnotationGroupSavedObject;
function savedObjectToEventAnnotationGroupSavedObject(
savedObject:
| SavedObject<EventAnnotationGroupSavedObjectAttributes>
| PartialSavedObject<EventAnnotationGroupSavedObjectAttributes>
): 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<EventAnnotationGroupSavedObject, PartialEventAnnotationGroupSavedObject>
{
constructor() {}
async get(ctx: StorageContext, id: string): Promise<EventAnnotationGroupGetOut> {
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<EventAnnotationGroupSavedObjectAttributes>(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<never> {
// 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<EventAnnotationGroupCreateOut> {
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<EventAnnotationGroupSavedObjectAttributes>(
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<EventAnnotationGroupUpdateOut> {
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<EventAnnotationGroupSavedObjectAttributes>(
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<EventAnnotationGroupDeleteOut> {
const soClient = await savedObjectClientFromRequest(ctx);
await soClient.delete(SO_TYPE, id);
return { success: true };
}
async search(
ctx: StorageContext,
query: SearchQuery,
options: EventAnnotationGroupSearchQuery = {}
): Promise<EventAnnotationGroupSearchOut> {
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<EventAnnotationGroupSavedObjectAttributes>(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;
}
}

View file

@ -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';

View file

@ -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<object, object> {
setupSavedObjects(core);
dependencies.contentManagement.register({
id: CONTENT_ID,
storage: new EventAnnotationGroupStorage(),
version: {
latest: LATEST_VERSION,
},
});
return {};
}

View file

@ -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}`,

View file

@ -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/**/*",