diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json
index fefbd780db750..adef16572e777 100644
--- a/packages/kbn-check-mappings-update-cli/current_fields.json
+++ b/packages/kbn-check-mappings-update-cli/current_fields.json
@@ -787,6 +787,7 @@
"description",
"state",
"title",
+ "version",
"visualizationType"
],
"lens-ui-telemetry": [
diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json
index 54d30d0713d06..f1b61d074d50d 100644
--- a/packages/kbn-check-mappings-update-cli/current_mappings.json
+++ b/packages/kbn-check-mappings-update-cli/current_mappings.json
@@ -2610,6 +2610,9 @@
"title": {
"type": "text"
},
+ "version": {
+ "type": "integer"
+ },
"visualizationType": {
"type": "keyword"
}
diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
index 4e9de5d1352f1..b45e8ebbf85d3 100644
--- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts
@@ -138,7 +138,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"inventory-view": "e125c6e6e49729055421e7b3a0544f24330d8dc6",
"kql-telemetry": "92d6357aa3ce28727492f86a54783f802dc38893",
"legacy-url-alias": "9b8cca3fbb2da46fd12823d3cd38fdf1c9f24bc8",
- "lens": "6fa6bdc5de12859815de6e50488fa2a7b038278a",
+ "lens": "17438090d0184ea2d834692aec084f7ad4ded710",
"lens-ui-telemetry": "d6c4e330d170eefc6214dbf77a53de913fa3eebc",
"links": "53ae5a770d69eee34d842617be761cd059ab4b51",
"maintenance-window": "f3f19d1828e91418d13703ce6009e9c76a1686f9",
@@ -843,8 +843,9 @@ describe('checking migration metadata changes on all registered SO types', () =>
"legacy-url-alias|8.2.0: 4c58be29493363cb48eb37fa5612fb3fb0eed77e",
"================================================================",
"lens|global: 0267d37678684c5f234bd74f0f6ae2a5f6180eb4",
- "lens|mappings: ccf7f3aea93efcf8d26faffa63e4bf6f0748ea80",
+ "lens|mappings: a7f9fb8ae5efb47f6ee930168cb1cdd221cd9d91",
"lens|schemas: da39a3ee5e6b4b0d3255bfef95601890afd80709",
+ "lens|10.1.0: 8f6fbdde199327bbfaba97d1d79ae3c9b9914b18",
"lens|8.9.0: f6592efb2500e39d1d31b1effdfad7d7ad1eca32",
"lens|8.6.0: 54c152a2584673672445346cf69d72bda587cc52",
"lens|8.5.0: 54c152a2584673672445346cf69d72bda587cc52",
@@ -1304,7 +1305,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"inventory-view": "10.2.0",
"kql-telemetry": "10.0.0",
"legacy-url-alias": "10.0.0",
- "lens": "10.0.0",
+ "lens": "10.1.0",
"lens-ui-telemetry": "10.0.0",
"links": "10.0.0",
"maintenance-window": "10.2.0",
@@ -1451,7 +1452,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"inventory-view": "10.2.0",
"kql-telemetry": "0.0.0",
"legacy-url-alias": "8.2.0",
- "lens": "8.9.0",
+ "lens": "10.1.0",
"lens-ui-telemetry": "0.0.0",
"links": "0.0.0",
"maintenance-window": "10.2.0",
diff --git a/src/platform/packages/shared/kbn-config-schema/src/types/object_type.ts b/src/platform/packages/shared/kbn-config-schema/src/types/object_type.ts
index 43344d62dbb3a..8d0bf8f991cd3 100644
--- a/src/platform/packages/shared/kbn-config-schema/src/types/object_type.ts
+++ b/src/platform/packages/shared/kbn-config-schema/src/types/object_type.ts
@@ -227,8 +227,6 @@ export class ObjectType
extends Type>
/**
* Return the schema for this object's underlying properties
- *
- * @internal should only be used internal for type reflection
*/
public getPropSchemas(): P {
return this.props;
diff --git a/src/platform/packages/shared/kbn-content-management-utils/index.ts b/src/platform/packages/shared/kbn-content-management-utils/index.ts
index 46499af9736ce..0d74f2979f2d2 100644
--- a/src/platform/packages/shared/kbn-content-management-utils/index.ts
+++ b/src/platform/packages/shared/kbn-content-management-utils/index.ts
@@ -11,4 +11,3 @@ export type * from './src/types';
export * from './src/schema';
export * from './src/saved_object_content_storage';
export * from './src/utils';
-export * from './src/msearch';
diff --git a/src/platform/packages/shared/kbn-content-management-utils/kibana.jsonc b/src/platform/packages/shared/kbn-content-management-utils/kibana.jsonc
index 3125cc30da6a0..2db27a2a904c7 100644
--- a/src/platform/packages/shared/kbn-content-management-utils/kibana.jsonc
+++ b/src/platform/packages/shared/kbn-content-management-utils/kibana.jsonc
@@ -1,5 +1,5 @@
{
- "type": "shared-server",
+ "type": "shared-common",
"id": "@kbn/content-management-utils",
"owner": [
"@elastic/kibana-data-discovery"
diff --git a/src/platform/packages/shared/kbn-content-management-utils/src/msearch.ts b/src/platform/packages/shared/kbn-content-management-utils/src/msearch.ts
deleted file mode 100644
index 70187ad574a39..0000000000000
--- a/src/platform/packages/shared/kbn-content-management-utils/src/msearch.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import Boom from '@hapi/boom';
-import { pick } from 'lodash';
-
-import type { StorageContext } from '@kbn/content-management-plugin/server';
-
-import type {
- SavedObjectsFindResult,
- SavedObject,
- SavedObjectReference,
-} from '@kbn/core-saved-objects-api-server';
-
-import type { ServicesDefinitionSet, SOWithMetadata, SOWithMetadataPartial } from './types';
-
-type PartialSavedObject = Omit>, 'references'> & {
- references: SavedObjectReference[] | undefined;
-};
-
-interface GetMSearchParams {
- savedObjectType: string;
- cmServicesDefinition: ServicesDefinitionSet;
- allowedSavedObjectAttributes: string[];
-}
-
-function savedObjectToItem(
- savedObject: SavedObject | PartialSavedObject,
- allowedSavedObjectAttributes: string[]
-): SOWithMetadata | SOWithMetadataPartial {
- const {
- id,
- type,
- updated_at: updatedAt,
- created_at: createdAt,
- attributes,
- references,
- error,
- namespaces,
- version,
- managed,
- } = savedObject;
-
- return {
- id,
- type,
- managed,
- updatedAt,
- createdAt,
- attributes: pick(attributes, allowedSavedObjectAttributes),
- references,
- error,
- namespaces,
- version,
- };
-}
-
-export interface GetMSearchType {
- savedObjectType: string;
- toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult) => ReturnItem;
-}
-
-export const getMSearch = ({
- savedObjectType,
- cmServicesDefinition,
- allowedSavedObjectAttributes,
-}: GetMSearchParams) => {
- return {
- savedObjectType,
- toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult): ReturnItem => {
- const transforms = ctx.utils.getTransforms(cmServicesDefinition);
-
- // Validate DB response and DOWN transform to the request version
- const { value, error: resultError } = transforms.mSearch.out.result.down<
- ReturnItem,
- ReturnItem
- >(
- // Ran into a case where a schema was broken by a SO attribute that wasn't part of the definition
- // so we specify which attributes are allowed
- savedObjectToItem(
- savedObject as SavedObjectsFindResult,
- allowedSavedObjectAttributes
- )
- );
-
- if (resultError) {
- throw Boom.badRequest(`Invalid response. ${resultError.message}`);
- }
-
- return value;
- },
- };
-};
diff --git a/src/platform/packages/shared/kbn-content-management-utils/src/saved_object_content_storage.test.ts b/src/platform/packages/shared/kbn-content-management-utils/src/saved_object_content_storage.test.ts
index 23dbb7e4c7005..f771ef895a9ea 100644
--- a/src/platform/packages/shared/kbn-content-management-utils/src/saved_object_content_storage.test.ts
+++ b/src/platform/packages/shared/kbn-content-management-utils/src/saved_object_content_storage.test.ts
@@ -8,7 +8,7 @@
*/
import { SOContentStorage } from './saved_object_content_storage';
-import { CMCrudTypes } from './types';
+import { ContentManagementCrudTypes } from './types';
import { loggerMock, MockedLogger } from '@kbn/logging-mocks';
import { schema } from '@kbn/config-schema';
@@ -16,11 +16,18 @@ import type {
ContentManagementServicesDefinition as ServicesDefinition,
Version,
} from '@kbn/object-versioning';
-import { getContentManagmentServicesTransforms } from '@kbn/object-versioning';
+import { getContentManagementServicesTransforms } from '@kbn/object-versioning';
import { savedObjectSchema, objectTypeToGetResultSchema, createResultSchema } from './schema';
import { coreMock } from '@kbn/core/server/mocks';
-import type { SavedObject } from '@kbn/core/server';
+import type { RequestHandlerContext, SavedObject } from '@kbn/core/server';
+import { mockRouter } from '@kbn/core-http-router-server-mocks';
+
+interface MockAttributes {
+ title: string;
+ description: string | null;
+}
+type MockCrudTypes = ContentManagementCrudTypes<'content-id', MockAttributes, {}, {}, {}>;
const testAttributesSchema = schema.object(
{
@@ -74,9 +81,9 @@ export const cmServicesDefinition: { [version: Version]: ServicesDefinition } =
1: serviceDefinition,
};
-const transforms = getContentManagmentServicesTransforms(cmServicesDefinition, 1);
+const transforms = getContentManagementServicesTransforms(cmServicesDefinition, 1);
-class TestSOContentStorage extends SOContentStorage {
+class TestSOContentStorage extends SOContentStorage {
constructor({
throwOnResultValidationError,
logger,
@@ -94,12 +101,12 @@ class TestSOContentStorage extends SOContentStorage {
const setup = ({ storage }: { storage?: TestSOContentStorage } = {}) => {
storage = storage ?? new TestSOContentStorage();
+ const mockRequest = mockRouter.createFakeKibanaRequest({});
const requestHandlerCoreContext = coreMock.createRequestHandlerContext();
-
- const requestHandlerContext = {
+ const requestHandlerContext = jest.mocked({
core: Promise.resolve(requestHandlerCoreContext),
resolve: jest.fn(),
- };
+ });
return {
get: (mockSavedObject: SavedObject) => {
@@ -110,6 +117,7 @@ const setup = ({ storage }: { storage?: TestSOContentStorage } = {}) => {
return storage!.get(
{
+ request: mockRequest,
requestHandlerContext,
version: {
request: 1,
@@ -122,11 +130,12 @@ const setup = ({ storage }: { storage?: TestSOContentStorage } = {}) => {
mockSavedObject.id
);
},
- create: (mockSavedObject: SavedObject<{}>) => {
+ create: (mockSavedObject: SavedObject) => {
requestHandlerCoreContext.savedObjects.client.create.mockResolvedValue(mockSavedObject);
return storage!.create(
{
+ request: mockRequest,
requestHandlerContext,
version: {
request: 1,
@@ -140,11 +149,12 @@ const setup = ({ storage }: { storage?: TestSOContentStorage } = {}) => {
{}
);
},
- update: (mockSavedObject: SavedObject<{}>) => {
+ update: (mockSavedObject: SavedObject) => {
requestHandlerCoreContext.savedObjects.client.update.mockResolvedValue(mockSavedObject);
return storage!.update(
{
+ request: mockRequest,
requestHandlerContext,
version: {
request: 1,
@@ -159,7 +169,7 @@ const setup = ({ storage }: { storage?: TestSOContentStorage } = {}) => {
{}
);
},
- search: (mockSavedObject: SavedObject<{}>) => {
+ search: (mockSavedObject: SavedObject) => {
requestHandlerCoreContext.savedObjects.client.find.mockResolvedValue({
saved_objects: [{ ...mockSavedObject, score: 100 }],
total: 1,
@@ -169,6 +179,7 @@ const setup = ({ storage }: { storage?: TestSOContentStorage } = {}) => {
return storage!.search(
{
+ request: mockRequest,
requestHandlerContext,
version: {
request: 1,
@@ -182,9 +193,10 @@ const setup = ({ storage }: { storage?: TestSOContentStorage } = {}) => {
{}
);
},
- mSearch: async (mockSavedObject: SavedObject<{}>) => {
+ mSearch: async (mockSavedObject: SavedObject) => {
return storage!.mSearch!.toItemResult(
{
+ request: mockRequest,
requestHandlerContext,
version: {
request: 1,
diff --git a/src/platform/packages/shared/kbn-content-management-utils/src/saved_object_content_storage.ts b/src/platform/packages/shared/kbn-content-management-utils/src/saved_object_content_storage.ts
index 72af69544b675..3a4bbedd11270 100644
--- a/src/platform/packages/shared/kbn-content-management-utils/src/saved_object_content_storage.ts
+++ b/src/platform/packages/shared/kbn-content-management-utils/src/saved_object_content_storage.ts
@@ -32,66 +32,10 @@ import type {
} from './types';
import { tagsToFindOptions } from './utils';
-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 savedObjectToItem(
- savedObject: SavedObject,
- allowedSavedObjectAttributes: string[],
- partial: false
-): Item;
-
-function savedObjectToItem(
- savedObject: PartialSavedObject,
- allowedSavedObjectAttributes: string[],
- partial: true
-): PartialItem;
-
-function savedObjectToItem(
- savedObject: SavedObject | PartialSavedObject,
- allowedSavedObjectAttributes: string[]
-): SOWithMetadata | SOWithMetadataPartial {
- const {
- id,
- type,
- updated_at: updatedAt,
- updated_by: updatedBy,
- created_at: createdAt,
- created_by: createdBy,
- attributes,
- references,
- error,
- namespaces,
- version,
- managed,
- } = savedObject;
-
- return {
- id,
- type,
- managed,
- updatedBy,
- updatedAt,
- createdAt,
- createdBy,
- attributes: pick(attributes, allowedSavedObjectAttributes),
- references,
- error,
- namespaces,
- version,
- };
-}
-
export interface SearchArgsToSOFindOptionsOptionsDefault {
fields?: string[];
searchFields?: string[];
@@ -138,7 +82,7 @@ export interface SOContentStorageConstructorParams {
savedObjectType: string;
cmServicesDefinition: ServicesDefinitionSet;
// this is necessary since unexpected saved object attributes could cause schema validation to fail
- allowedSavedObjectAttributes: string[];
+ allowedSavedObjectAttributes: Array;
createArgsToSoCreateOptions?: CreateArgsToSoCreateOptions;
updateArgsToSoUpdateOptions?: UpdateArgsToSoUpdateOptions;
searchArgsToSOFindOptions?: SearchArgsToSOFindOptions;
@@ -193,10 +137,8 @@ export abstract class SOContentStorage
toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult): Types['Item'] => {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
- const contentItem = savedObjectToItem(
- savedObject as SavedObjectsFindResult,
- this.allowedSavedObjectAttributes,
- false
+ const contentItem = this.savedObjectToItem(
+ savedObject as SavedObjectsFindResult
);
const validationError = transforms.mSearch.out.result.validate(contentItem);
@@ -228,24 +170,73 @@ export abstract class SOContentStorage
}
}
- private throwOnResultValidationError: boolean;
- private logger: Logger;
- private savedObjectType: SOContentStorageConstructorParams['savedObjectType'];
- private cmServicesDefinition: SOContentStorageConstructorParams['cmServicesDefinition'];
- private createArgsToSoCreateOptions: CreateArgsToSoCreateOptions;
- private updateArgsToSoUpdateOptions: UpdateArgsToSoUpdateOptions;
- private searchArgsToSOFindOptions: SearchArgsToSOFindOptions;
- private allowedSavedObjectAttributes: string[];
+ protected static getSOClientFromRequest = async (ctx: StorageContext) => {
+ if (!ctx.requestHandlerContext) {
+ throw new Error('Storage context.requestHandlerContext missing.');
+ }
+
+ const { savedObjects } = await ctx.requestHandlerContext.core;
+ return savedObjects.client;
+ };
+
+ protected readonly throwOnResultValidationError: boolean;
+ protected readonly logger: Logger;
+ protected readonly savedObjectType: SOContentStorageConstructorParams['savedObjectType'];
+ protected readonly cmServicesDefinition: SOContentStorageConstructorParams['cmServicesDefinition'];
+ protected readonly createArgsToSoCreateOptions: CreateArgsToSoCreateOptions;
+ protected readonly updateArgsToSoUpdateOptions: UpdateArgsToSoUpdateOptions;
+ protected readonly searchArgsToSOFindOptions: SearchArgsToSOFindOptions;
+ protected readonly allowedSavedObjectAttributes: Array;
+
+ protected savedObjectToItem(savedObject: SavedObject): Types['Item'];
+ protected savedObjectToItem(
+ savedObject: PartialSavedObject,
+ partial: true
+ ): Types['PartialItem'];
+ protected savedObjectToItem(
+ savedObject: SavedObject | PartialSavedObject
+ ): SOWithMetadata | SOWithMetadataPartial {
+ const {
+ id,
+ type,
+ updated_at: updatedAt,
+ updated_by: updatedBy,
+ created_at: createdAt,
+ created_by: createdBy,
+ attributes,
+ references,
+ error,
+ namespaces,
+ version,
+ managed,
+ } = savedObject;
+
+ return {
+ id,
+ type,
+ managed,
+ updatedBy,
+ updatedAt,
+ createdAt,
+ createdBy,
+ attributes: pick(attributes, this.allowedSavedObjectAttributes),
+ references,
+ error,
+ namespaces,
+ version,
+ };
+ }
mSearch?: {
savedObjectType: string;
+ // TODO: fix this typing, this does not always return the full Item
toItemResult: (ctx: StorageContext, savedObject: SavedObjectsFindResult) => Types['Item'];
additionalSearchFields?: string[];
};
async get(ctx: StorageContext, id: string): Promise {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
- const soClient = await savedObjectClientFromRequest(ctx);
+ const soClient = await SOContentStorage.getSOClientFromRequest(ctx);
// Save data in DB
const {
@@ -256,7 +247,7 @@ export abstract class SOContentStorage
} = await soClient.resolve(this.savedObjectType, id);
const response: Types['GetOut'] = {
- item: savedObjectToItem(savedObject, this.allowedSavedObjectAttributes, false),
+ item: this.savedObjectToItem(savedObject),
meta: {
aliasPurpose,
aliasTargetId,
@@ -301,7 +292,7 @@ export abstract class SOContentStorage
options: Types['CreateOptions']
): Promise {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
- const soClient = await savedObjectClientFromRequest(ctx);
+ const soClient = await SOContentStorage.getSOClientFromRequest(ctx);
// Validate input (data & options) & UP transform them to the latest version
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
@@ -330,7 +321,7 @@ export abstract class SOContentStorage
);
const result = {
- item: savedObjectToItem(savedObject, this.allowedSavedObjectAttributes, false),
+ item: this.savedObjectToItem(savedObject),
};
const validationError = transforms.create.out.result.validate(result);
@@ -366,7 +357,7 @@ export abstract class SOContentStorage
options: Types['UpdateOptions']
): Promise {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
- const soClient = await savedObjectClientFromRequest(ctx);
+ const soClient = await SOContentStorage.getSOClientFromRequest(ctx);
// Validate input (data & options) & UP transform them to the latest version
const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
@@ -396,7 +387,7 @@ export abstract class SOContentStorage
);
const result = {
- item: savedObjectToItem(partialSavedObject, this.allowedSavedObjectAttributes, true),
+ item: this.savedObjectToItem(partialSavedObject, true),
};
const validationError = transforms.update.out.result.validate(result);
@@ -431,7 +422,7 @@ export abstract class SOContentStorage
// force is necessary to delete saved objects that exist in multiple namespaces
options?: { force: boolean }
): Promise {
- const soClient = await savedObjectClientFromRequest(ctx);
+ const soClient = await SOContentStorage.getSOClientFromRequest(ctx);
await soClient.delete(this.savedObjectType, id, { force: options?.force ?? false });
return { success: true };
}
@@ -442,7 +433,7 @@ export abstract class SOContentStorage
options: Types['SearchOptions'] = {}
): Promise {
const transforms = ctx.utils.getTransforms(this.cmServicesDefinition);
- const soClient = await savedObjectClientFromRequest(ctx);
+ const soClient = await SOContentStorage.getSOClientFromRequest(ctx);
// Validate and UP transform the options
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
@@ -461,9 +452,7 @@ export abstract class SOContentStorage
// Execute the query in the DB
const soResponse = await soClient.find(soQuery);
const response = {
- hits: soResponse.saved_objects.map((so) =>
- savedObjectToItem(so, this.allowedSavedObjectAttributes, false)
- ),
+ hits: soResponse.saved_objects.map((so) => this.savedObjectToItem(so)),
pagination: {
total: soResponse.total,
},
diff --git a/src/platform/packages/shared/kbn-content-management-utils/src/schema.ts b/src/platform/packages/shared/kbn-content-management-utils/src/schema.ts
index c3ba8e91df933..c36eb59985741 100644
--- a/src/platform/packages/shared/kbn-content-management-utils/src/schema.ts
+++ b/src/platform/packages/shared/kbn-content-management-utils/src/schema.ts
@@ -18,7 +18,7 @@ export const apiError = schema.object({
export const referenceSchema = schema.object(
{
- name: schema.maybe(schema.string()),
+ name: schema.string(),
type: schema.string(),
id: schema.string(),
},
@@ -27,7 +27,7 @@ export const referenceSchema = schema.object(
export const referencesSchema = schema.arrayOf(referenceSchema);
-export const savedObjectSchema = (attributesSchema: ObjectType) =>
+export const savedObjectSchema = >(attributesSchema: T) =>
schema.object(
{
id: schema.string(),
@@ -35,16 +35,19 @@ export const savedObjectSchema = (attributesSchema: ObjectType) =>
version: schema.maybe(schema.string()),
createdAt: schema.maybe(schema.string()),
updatedAt: schema.maybe(schema.string()),
+ createdBy: schema.maybe(schema.string()),
+ updatedBy: schema.maybe(schema.string()),
error: schema.maybe(apiError),
attributes: attributesSchema,
references: referencesSchema,
namespaces: schema.maybe(schema.arrayOf(schema.string())),
originId: schema.maybe(schema.string()),
+ managed: schema.maybe(schema.boolean()),
},
{ unknowns: 'allow' }
);
-export const objectTypeToGetResultSchema = (soSchema: ObjectType) =>
+export const objectTypeToGetResultSchema = >(soSchema: T) =>
schema.object(
{
item: soSchema,
@@ -111,15 +114,34 @@ export const updateOptionsSchema = {
references: schema.maybe(referencesSchema),
version: schema.maybe(schema.string()),
refresh: schema.maybe(schema.oneOf([schema.boolean(), schema.literal('wait_for')])),
- upsert: (attributesSchema: ObjectType) => schema.maybe(savedObjectSchema(attributesSchema)),
+ upsert: >(attributesSchema: T) =>
+ schema.maybe(savedObjectSchema(attributesSchema)),
retryOnConflict: schema.maybe(schema.number()),
mergeAttributes: schema.maybe(schema.boolean()),
};
-export const createResultSchema = (soSchema: ObjectType) =>
+export const createResultSchema = >(soSchema: T) =>
schema.object(
{
item: soSchema,
},
{ unknowns: 'forbid' }
);
+
+export const searchResultSchema = , M extends ObjectType = never>(
+ soSchema: T,
+ meta?: M
+) =>
+ schema.object(
+ {
+ hits: schema.arrayOf(soSchema),
+ pagination: schema.object({
+ total: schema.number(),
+ cursor: schema.maybe(schema.string()),
+ }),
+ ...(meta && {
+ meta,
+ }),
+ },
+ { unknowns: 'forbid' }
+ );
diff --git a/src/platform/packages/shared/kbn-content-management-utils/tsconfig.json b/src/platform/packages/shared/kbn-content-management-utils/tsconfig.json
index 1f21540e1a90a..0135dc47df77d 100644
--- a/src/platform/packages/shared/kbn-content-management-utils/tsconfig.json
+++ b/src/platform/packages/shared/kbn-content-management-utils/tsconfig.json
@@ -24,5 +24,6 @@
"@kbn/logging",
"@kbn/logging-mocks",
"@kbn/core",
+ "@kbn/core-http-router-server-mocks",
]
}
diff --git a/src/platform/packages/shared/kbn-object-versioning/README.md b/src/platform/packages/shared/kbn-object-versioning/README.md
index 37db4dc7e5f5a..30972d18253ed 100644
--- a/src/platform/packages/shared/kbn-object-versioning/README.md
+++ b/src/platform/packages/shared/kbn-object-versioning/README.md
@@ -1,3 +1,3 @@
# @kbn/object-versioning
-Empty package generated by @kbn/generate
+A package to handle versioning of Content Management.
diff --git a/src/platform/packages/shared/kbn-object-versioning/index.ts b/src/platform/packages/shared/kbn-object-versioning/index.ts
index 31bcc9117bfb7..6818362119397 100644
--- a/src/platform/packages/shared/kbn-object-versioning/index.ts
+++ b/src/platform/packages/shared/kbn-object-versioning/index.ts
@@ -9,7 +9,7 @@
export {
initTransform,
- getContentManagmentServicesTransforms,
+ getContentManagementServicesTransforms,
compileServiceDefinitions,
} from './lib';
diff --git a/src/platform/packages/shared/kbn-object-versioning/kibana.jsonc b/src/platform/packages/shared/kbn-object-versioning/kibana.jsonc
index 5d20eefe37bd8..25a91b87ff240 100644
--- a/src/platform/packages/shared/kbn-object-versioning/kibana.jsonc
+++ b/src/platform/packages/shared/kbn-object-versioning/kibana.jsonc
@@ -1,5 +1,5 @@
{
- "type": "shared-server",
+ "type": "shared-common",
"id": "@kbn/object-versioning",
"owner": [
"@elastic/appex-sharedux"
diff --git a/src/platform/packages/shared/kbn-object-versioning/lib/index.ts b/src/platform/packages/shared/kbn-object-versioning/lib/index.ts
index 814a49b9828b1..c3b09c5e9d23c 100644
--- a/src/platform/packages/shared/kbn-object-versioning/lib/index.ts
+++ b/src/platform/packages/shared/kbn-object-versioning/lib/index.ts
@@ -10,7 +10,7 @@
export { initTransform } from './object_transform';
export {
- getTransforms as getContentManagmentServicesTransforms,
+ getTransforms as getContentManagementServicesTransforms,
compile as compileServiceDefinitions,
} from './content_management_services_versioning';
diff --git a/src/platform/packages/shared/kbn-object-versioning/lib/object_transform.ts b/src/platform/packages/shared/kbn-object-versioning/lib/object_transform.ts
index f95cf22b39213..158989ac44ad3 100644
--- a/src/platform/packages/shared/kbn-object-versioning/lib/object_transform.ts
+++ b/src/platform/packages/shared/kbn-object-versioning/lib/object_transform.ts
@@ -57,7 +57,7 @@ const getTransformFns = (
let fn: ObjectTransform | undefined;
if (to > from) {
while (i <= to) {
- fn = migrationDefinition[i].up;
+ fn = migrationDefinition[i]?.up;
if (fn) {
fns.push(fn);
}
@@ -65,7 +65,7 @@ const getTransformFns = (
}
} else if (to < from) {
while (i >= to) {
- fn = migrationDefinition[i].down;
+ fn = migrationDefinition[i]?.down;
if (fn) {
fns.push(fn);
}
@@ -126,7 +126,7 @@ export const initTransform =
try {
if (!migrationDefinition[requestVersion]) {
return {
- error: new Error(`Unvalid version to up transform from [${requestVersion}].`),
+ error: new Error(`Invalid version to up transform from [${requestVersion}].`),
value: null,
};
}
@@ -142,7 +142,7 @@ export const initTransform =
if (!migrationDefinition[targetVersion]) {
return {
- error: new Error(`Unvalid version to up transform to [${to}].`),
+ error: new Error(`Invalid version to up transform to [${to}].`),
value: null,
};
}
@@ -170,21 +170,22 @@ export const initTransform =
try {
if (!migrationDefinition[requestVersion]) {
return {
- error: new Error(`Unvalid version to down transform to [${requestVersion}].`),
+ error: new Error(`Invalid version to down transform to [${requestVersion}].`),
value: null,
};
}
const fromVersion = getVersion(from);
- if (!migrationDefinition[fromVersion]) {
+ // Only allow missing from definition for initial versioning (i.e. v0 → v1)
+ if (!migrationDefinition[fromVersion] && fromVersion !== 0) {
return {
- error: new Error(`Unvalid version to down transform from [${from}].`),
+ error: new Error(`Invalid version to down transform from [${from}].`),
value: null,
};
}
- if (validate) {
+ if (validate && fromVersion !== 0) {
const error = validateFn(obj, fromVersion);
if (error) {
return { error, value: null };
diff --git a/src/platform/packages/shared/kbn-object-versioning/lib/types.ts b/src/platform/packages/shared/kbn-object-versioning/lib/types.ts
index a727ef54b47bc..589258014cb23 100644
--- a/src/platform/packages/shared/kbn-object-versioning/lib/types.ts
+++ b/src/platform/packages/shared/kbn-object-versioning/lib/types.ts
@@ -53,7 +53,7 @@ export interface ObjectTransforms<
}
) => TransformReturn;
down: (
- obj: DownIn,
+ obj: I,
version?: Version | 'latest',
options?: {
/** Validate the object _before_ down transform */
diff --git a/src/platform/packages/shared/kbn-visualization-utils/src/get_lens_attributes.ts b/src/platform/packages/shared/kbn-visualization-utils/src/get_lens_attributes.ts
index eb5666cc790f9..5ec59efad3660 100644
--- a/src/platform/packages/shared/kbn-visualization-utils/src/get_lens_attributes.ts
+++ b/src/platform/packages/shared/kbn-visualization-utils/src/get_lens_attributes.ts
@@ -23,6 +23,7 @@ export const getLensAttributesFromSuggestion = ({
suggestion: Suggestion | undefined;
dataView?: DataView;
}): {
+ // TODO: make these types reference the actual attributes
references: Array<{ name: string; id: string; type: string }>;
visualizationType: string;
state: {
@@ -32,6 +33,7 @@ export const getLensAttributesFromSuggestion = ({
filters: Filter[];
};
title: string;
+ version: 1;
} => {
const suggestionDatasourceState = Object.assign({}, suggestion?.datasourceState);
const suggestionVisualizationState = Object.assign({}, suggestion?.visualizationState);
@@ -65,6 +67,7 @@ export const getLensAttributesFromSuggestion = ({
}),
},
visualizationType: suggestion ? suggestion.visualizationId : 'lnsXY',
+ version: 1 as const,
};
return attributes;
};
diff --git a/src/platform/plugins/private/event_annotation_listing/public/components/group_editor_flyout/lens_attributes.ts b/src/platform/plugins/private/event_annotation_listing/public/components/group_editor_flyout/lens_attributes.ts
index 416c18c7f457f..05224e19d8c12 100644
--- a/src/platform/plugins/private/event_annotation_listing/public/components/group_editor_flyout/lens_attributes.ts
+++ b/src/platform/plugins/private/event_annotation_listing/public/components/group_editor_flyout/lens_attributes.ts
@@ -143,6 +143,7 @@ export const getLensAttributes = (group: EventAnnotationGroupConfig, timeField:
name: `indexpattern-datasource-layer-${DATA_LAYER_ID}`,
},
],
+ version: 1 as const,
} as TypedLensByValueInput['attributes']);
export const getCurrentTimeField = (attributes: TypedLensByValueInput['attributes']) => {
diff --git a/src/platform/plugins/private/links/server/content_management/links_storage.ts b/src/platform/plugins/private/links/server/content_management/links_storage.ts
index 09228127b80b3..21e682b61893c 100644
--- a/src/platform/plugins/private/links/server/content_management/links_storage.ts
+++ b/src/platform/plugins/private/links/server/content_management/links_storage.ts
@@ -96,6 +96,7 @@ export class LinksStorage {
// Validate response and DOWN transform to the request version
const { value, error: resultError } = transforms.get.out.result.down(
+ // @ts-expect-error - need to fix response.item type here
response,
undefined, // do not override version
{ validate: false } // validation is done above
@@ -231,6 +232,7 @@ export class LinksStorage {
LinksUpdateOut,
LinksUpdateOut
>(
+ // @ts-expect-error - need to fix item type here
{ item },
undefined, // do not override version
{ validate: false } // validation is done above
diff --git a/src/platform/plugins/shared/content_management/common/rpc/bulk_get.ts b/src/platform/plugins/shared/content_management/common/rpc/bulk_get.ts
index e3c9ea127e35d..bfdb1ff8a8db9 100644
--- a/src/platform/plugins/shared/content_management/common/rpc/bulk_get.ts
+++ b/src/platform/plugins/shared/content_management/common/rpc/bulk_get.ts
@@ -14,7 +14,7 @@ import { GetResult, getResultSchema } from './get';
import type { ProcedureSchemas } from './types';
-export const bulkGetSchemas: ProcedureSchemas = {
+export const bulkGetSchemas = {
in: schema.object(
{
contentTypeId: schema.string(),
@@ -31,7 +31,7 @@ export const bulkGetSchemas: ProcedureSchemas = {
},
{ unknowns: 'forbid' }
),
-};
+} satisfies ProcedureSchemas;
export interface BulkGetIn {
contentTypeId: T;
diff --git a/src/platform/plugins/shared/content_management/common/rpc/create.ts b/src/platform/plugins/shared/content_management/common/rpc/create.ts
index 33f33637dbf4e..8c26438a3c7a7 100644
--- a/src/platform/plugins/shared/content_management/common/rpc/create.ts
+++ b/src/platform/plugins/shared/content_management/common/rpc/create.ts
@@ -14,7 +14,7 @@ import { versionSchema } from './constants';
import type { ItemResult, ProcedureSchemas } from './types';
-export const createSchemas: ProcedureSchemas = {
+export const createSchemas = {
in: schema.object(
{
contentTypeId: schema.string(),
@@ -32,7 +32,7 @@ export const createSchemas: ProcedureSchemas = {
},
{ unknowns: 'forbid' }
),
-};
+} satisfies ProcedureSchemas;
export interface CreateIn<
T extends string = string,
diff --git a/src/platform/plugins/shared/content_management/common/rpc/delete.ts b/src/platform/plugins/shared/content_management/common/rpc/delete.ts
index 7b67b17ff1f6d..cad122815bc28 100644
--- a/src/platform/plugins/shared/content_management/common/rpc/delete.ts
+++ b/src/platform/plugins/shared/content_management/common/rpc/delete.ts
@@ -13,7 +13,7 @@ import { versionSchema } from './constants';
import type { ProcedureSchemas } from './types';
-export const deleteSchemas: ProcedureSchemas = {
+export const deleteSchemas = {
in: schema.object(
{
contentTypeId: schema.string(),
@@ -35,7 +35,7 @@ export const deleteSchemas: ProcedureSchemas = {
},
{ unknowns: 'forbid' }
),
-};
+} satisfies ProcedureSchemas;
export interface DeleteIn {
contentTypeId: T;
diff --git a/src/platform/plugins/shared/content_management/common/rpc/get.ts b/src/platform/plugins/shared/content_management/common/rpc/get.ts
index 6aa51279c5318..bbb7ac7cb1b29 100644
--- a/src/platform/plugins/shared/content_management/common/rpc/get.ts
+++ b/src/platform/plugins/shared/content_management/common/rpc/get.ts
@@ -22,7 +22,7 @@ export const getResultSchema = schema.object(
{ unknowns: 'forbid' }
);
-export const getSchemas: ProcedureSchemas = {
+export const getSchemas = {
in: schema.object(
{
contentTypeId: schema.string(),
@@ -33,7 +33,7 @@ export const getSchemas: ProcedureSchemas = {
{ unknowns: 'forbid' }
),
out: getResultSchema,
-};
+} satisfies ProcedureSchemas;
export interface GetIn {
id: string;
diff --git a/src/platform/plugins/shared/content_management/common/rpc/msearch.ts b/src/platform/plugins/shared/content_management/common/rpc/msearch.ts
index 7020b66ff94c6..de2d15d713a5a 100644
--- a/src/platform/plugins/shared/content_management/common/rpc/msearch.ts
+++ b/src/platform/plugins/shared/content_management/common/rpc/msearch.ts
@@ -14,7 +14,7 @@ import { searchQuerySchema, searchResultSchema, SearchQuery, SearchResult } from
import type { ProcedureSchemas } from './types';
-export const mSearchSchemas: ProcedureSchemas = {
+export const mSearchSchemas = {
in: schema.object(
{
contentTypes: schema.arrayOf(
@@ -36,7 +36,7 @@ export const mSearchSchemas: ProcedureSchemas = {
},
{ unknowns: 'forbid' }
),
-};
+} satisfies ProcedureSchemas;
export type MSearchQuery = SearchQuery;
diff --git a/src/platform/plugins/shared/content_management/common/rpc/rpc.ts b/src/platform/plugins/shared/content_management/common/rpc/rpc.ts
index 5ddb7608fb5f9..b392d21acda3e 100644
--- a/src/platform/plugins/shared/content_management/common/rpc/rpc.ts
+++ b/src/platform/plugins/shared/content_management/common/rpc/rpc.ts
@@ -17,9 +17,7 @@ import { deleteSchemas } from './delete';
import { searchSchemas } from './search';
import { mSearchSchemas } from './msearch';
-export const schemas: {
- [key in ProcedureName]: ProcedureSchemas;
-} = {
+export const schemas = {
get: getSchemas,
bulkGet: bulkGetSchemas,
create: createSchemas,
@@ -27,4 +25,6 @@ export const schemas: {
delete: deleteSchemas,
search: searchSchemas,
mSearch: mSearchSchemas,
+} satisfies {
+ [key in ProcedureName]: ProcedureSchemas;
};
diff --git a/src/platform/plugins/shared/content_management/common/rpc/search.ts b/src/platform/plugins/shared/content_management/common/rpc/search.ts
index 1d79d115b71d0..3f977450d81b8 100644
--- a/src/platform/plugins/shared/content_management/common/rpc/search.ts
+++ b/src/platform/plugins/shared/content_management/common/rpc/search.ts
@@ -40,7 +40,7 @@ export const searchResultSchema = schema.object({
}),
});
-export const searchSchemas: ProcedureSchemas = {
+export const searchSchemas = {
in: schema.object(
{
contentTypeId: schema.string(),
@@ -58,7 +58,7 @@ export const searchSchemas: ProcedureSchemas = {
},
{ unknowns: 'forbid' }
),
-};
+} satisfies ProcedureSchemas;
export interface SearchQuery {
/** The text to search for */
diff --git a/src/platform/plugins/shared/content_management/common/rpc/update.ts b/src/platform/plugins/shared/content_management/common/rpc/update.ts
index ad581c0b97380..c387fd8c49067 100644
--- a/src/platform/plugins/shared/content_management/common/rpc/update.ts
+++ b/src/platform/plugins/shared/content_management/common/rpc/update.ts
@@ -14,7 +14,7 @@ import { versionSchema } from './constants';
import type { ItemResult, ProcedureSchemas } from './types';
-export const updateSchemas: ProcedureSchemas = {
+export const updateSchemas = {
in: schema.object(
{
contentTypeId: schema.string(),
@@ -33,7 +33,7 @@ export const updateSchemas: ProcedureSchemas = {
},
{ unknowns: 'forbid' }
),
-};
+} satisfies ProcedureSchemas;
export interface UpdateIn<
T extends string = string,
diff --git a/src/platform/plugins/shared/content_management/server/content_client/content_client_factory.ts b/src/platform/plugins/shared/content_management/server/content_client/content_client_factory.ts
index 090ed494bb500..1edb113c93699 100644
--- a/src/platform/plugins/shared/content_management/server/content_client/content_client_factory.ts
+++ b/src/platform/plugins/shared/content_management/server/content_client/content_client_factory.ts
@@ -7,12 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import type { Logger, KibanaRequest } from '@kbn/core/server';
import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
-import type { KibanaRequest } from '@kbn/core-http-server';
import { Version } from '@kbn/object-versioning';
import type { MSearchIn, MSearchOut } from '../../common';
-import type { ContentRegistry } from '../core';
+import type { ContentRegistry, StorageContext } from '../core';
import { MSearchService } from '../core/msearch';
import { getServiceObjectTransformFactory, getStorageContext } from '../utils';
import { ContentClient } from './content_client';
@@ -21,17 +21,19 @@ export const getContentClientFactory =
({ contentRegistry }: { contentRegistry: ContentRegistry }) =>
(contentTypeId: string) => {
const getForRequest = ({
+ request,
requestHandlerContext,
version,
}: {
- requestHandlerContext: RequestHandlerContext;
request: KibanaRequest;
+ requestHandlerContext: RequestHandlerContext;
version?: Version;
}) => {
const contentDefinition = contentRegistry.getDefinition(contentTypeId);
const storageContext = getStorageContext({
contentTypeId,
+ request,
version: version ?? contentDefinition.version.latest,
ctx: {
contentRegistry,
@@ -60,35 +62,46 @@ export const getMSearchClientFactory =
({
contentRegistry,
mSearchService,
+ logger,
}: {
contentRegistry: ContentRegistry;
mSearchService: MSearchService;
+ logger: Logger;
}) =>
({
requestHandlerContext,
+ request,
}: {
requestHandlerContext: RequestHandlerContext;
request: KibanaRequest;
}) => {
const msearch = async ({ contentTypes, query }: MSearchIn): Promise => {
- const contentTypesWithStorageContext = contentTypes.map(({ contentTypeId, version }) => {
+ const contentTypesWithStorageContext = contentTypes.reduce<
+ Array<{ contentTypeId: string; ctx: StorageContext }>
+ >((acc, { contentTypeId, version }) => {
const contentDefinition = contentRegistry.getDefinition(contentTypeId);
- const storageContext = getStorageContext({
- contentTypeId,
- version: version ?? contentDefinition.version.latest,
- ctx: {
- contentRegistry,
- requestHandlerContext,
- getTransformsFactory: getServiceObjectTransformFactory,
- },
- });
+ if (contentDefinition.storage.mSearch) {
+ const storageContext = getStorageContext({
+ request,
+ contentTypeId,
+ version: version ?? contentDefinition.version.latest,
+ ctx: {
+ contentRegistry,
+ requestHandlerContext,
+ getTransformsFactory: getServiceObjectTransformFactory,
+ },
+ });
+ acc.push({
+ contentTypeId,
+ ctx: storageContext,
+ });
+ } else {
+ logger.warn(`mSearch method missing for content type "${contentTypeId}" v${version}.`);
+ }
- return {
- contentTypeId,
- ctx: storageContext,
- };
- });
+ return acc;
+ }, []);
const result = await mSearchService.search(contentTypesWithStorageContext, query);
diff --git a/src/platform/plugins/shared/content_management/server/core/core.test.ts b/src/platform/plugins/shared/content_management/server/core/core.test.ts
index c076588c0fa52..d001c339db059 100644
--- a/src/platform/plugins/shared/content_management/server/core/core.test.ts
+++ b/src/platform/plugins/shared/content_management/server/core/core.test.ts
@@ -37,6 +37,8 @@ import type {
SearchItemError,
} from './event_types';
import { ContentStorage, ContentTypeDefinition, StorageContext } from './types';
+import { mockRouter } from '@kbn/core-http-router-server-mocks';
+import { RequestHandlerContext } from '@kbn/core/server';
const spyMsearch = jest.fn();
const getmSearchSpy = () => spyMsearch;
@@ -64,7 +66,8 @@ const setup = ({
latestVersion = 2,
}: { registerFooType?: boolean; storage?: ContentStorage; latestVersion?: number } = {}) => {
const ctx: StorageContext = {
- requestHandlerContext: {} as any,
+ request: mockRouter.createFakeKibanaRequest({}),
+ requestHandlerContext: jest.mocked({} as any),
version: {
latest: latestVersion,
request: 1,
diff --git a/src/platform/plugins/shared/content_management/server/core/core.ts b/src/platform/plugins/shared/content_management/server/core/core.ts
index 04c96ad737bf0..3717a8e15b87c 100644
--- a/src/platform/plugins/shared/content_management/server/core/core.ts
+++ b/src/platform/plugins/shared/content_management/server/core/core.ts
@@ -146,6 +146,7 @@ export class Core {
const msearchClientFactory = getMSearchClientFactory({
contentRegistry: this.contentRegistry,
mSearchService,
+ logger: this.ctx.logger,
});
const msearchClient = msearchClientFactory({ requestHandlerContext, request });
diff --git a/src/platform/plugins/shared/content_management/server/core/msearch.test.ts b/src/platform/plugins/shared/content_management/server/core/msearch.test.ts
index a60c77f66104f..2115e0db55161 100644
--- a/src/platform/plugins/shared/content_management/server/core/msearch.test.ts
+++ b/src/platform/plugins/shared/content_management/server/core/msearch.test.ts
@@ -13,6 +13,7 @@ import { ContentRegistry } from './registry';
import { createMockedStorage } from './mocks';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
import { StorageContext } from '.';
+import { mockRouter } from '@kbn/core-http-router-server-mocks';
const SEARCH_LISTING_LIMIT = 100;
const SEARCH_PER_PAGE = 10;
@@ -65,6 +66,7 @@ const setup = () => {
const mockStorageContext = (ctx: Partial = {}): StorageContext => {
return {
+ request: mockRouter.createFakeKibanaRequest({}),
requestHandlerContext: 'mockRequestHandlerContext' as any,
utils: 'mockUtils' as any,
version: {
diff --git a/src/platform/plugins/shared/content_management/server/core/types.ts b/src/platform/plugins/shared/content_management/server/core/types.ts
index cabd3b35b7261..1f28aa7c28714 100644
--- a/src/platform/plugins/shared/content_management/server/core/types.ts
+++ b/src/platform/plugins/shared/content_management/server/core/types.ts
@@ -15,6 +15,7 @@ import type {
} from '@kbn/object-versioning';
import type { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
+import { KibanaRequest } from '@kbn/core/server';
import type {
GetResult,
BulkGetResult,
@@ -32,6 +33,7 @@ export type StorageContextGetTransformFn = (
/** Context that is sent to all storage instance methods */
export interface StorageContext {
+ request: KibanaRequest;
/** The Core HTTP request handler context */
requestHandlerContext: RequestHandlerContext;
version: {
diff --git a/src/platform/plugins/shared/content_management/server/plugin.ts b/src/platform/plugins/shared/content_management/server/plugin.ts
index 0215f3d36771b..99cf11da13aee 100755
--- a/src/platform/plugins/shared/content_management/server/plugin.ts
+++ b/src/platform/plugins/shared/content_management/server/plugin.ts
@@ -68,7 +68,7 @@ export class ContentManagementPlugin
const { api: coreApi, contentRegistry } = this.core.setup();
const rpc = new RpcService();
- registerProcedures(rpc);
+ registerProcedures(rpc, this.logger);
const router = core.http.createRouter();
initRpcRoutes(procedureNames, router, {
diff --git a/src/platform/plugins/shared/content_management/server/rpc/procedures/all_procedures.ts b/src/platform/plugins/shared/content_management/server/rpc/procedures/all_procedures.ts
index 2a3b145fd50eb..55f566465289e 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/procedures/all_procedures.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/procedures/all_procedures.ts
@@ -7,6 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import type { Logger } from '@kbn/core/server';
+
import type { ProcedureName } from '../../../common';
import type { ProcedureDefinition } from '../rpc_service';
import type { Context } from '../types';
@@ -16,14 +18,18 @@ import { create } from './create';
import { update } from './update';
import { deleteProc } from './delete';
import { search } from './search';
-import { mSearch } from './msearch';
+import { getMSearch } from './msearch';
-export const procedures: { [key in ProcedureName]: ProcedureDefinition } = {
+export const getProcedures = (
+ logger: Logger
+): {
+ [key in ProcedureName]: ProcedureDefinition;
+} => ({
get,
bulkGet,
create,
update,
delete: deleteProc,
search,
- mSearch,
-};
+ mSearch: getMSearch(logger),
+});
diff --git a/src/platform/plugins/shared/content_management/server/rpc/procedures/bulk_get.test.ts b/src/platform/plugins/shared/content_management/server/rpc/procedures/bulk_get.test.ts
index 0bc8cc8c12f87..3a97bf4f2d5d7 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/procedures/bulk_get.test.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/procedures/bulk_get.test.ts
@@ -25,9 +25,9 @@ jest.mock('@kbn/object-versioning', () => {
const original = jest.requireActual('@kbn/object-versioning');
return {
...original,
- getContentManagmentServicesTransforms: (...args: any[]) => {
+ getContentManagementServicesTransforms: (...args: any[]) => {
spy()(...args);
- return original.getContentManagmentServicesTransforms(...args);
+ return original.getContentManagementServicesTransforms(...args);
},
};
});
diff --git a/src/platform/plugins/shared/content_management/server/rpc/procedures/create.test.ts b/src/platform/plugins/shared/content_management/server/rpc/procedures/create.test.ts
index 492be13d7de30..65a5d3ecc70d7 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/procedures/create.test.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/procedures/create.test.ts
@@ -25,9 +25,9 @@ jest.mock('@kbn/object-versioning', () => {
const original = jest.requireActual('@kbn/object-versioning');
return {
...original,
- getContentManagmentServicesTransforms: (...args: any[]) => {
+ getContentManagementServicesTransforms: (...args: any[]) => {
spy()(...args);
- return original.getContentManagmentServicesTransforms(...args);
+ return original.getContentManagementServicesTransforms(...args);
},
};
});
diff --git a/src/platform/plugins/shared/content_management/server/rpc/procedures/delete.test.ts b/src/platform/plugins/shared/content_management/server/rpc/procedures/delete.test.ts
index 8c70b906d4372..1771b443d4d26 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/procedures/delete.test.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/procedures/delete.test.ts
@@ -25,9 +25,9 @@ jest.mock('@kbn/object-versioning', () => {
const original = jest.requireActual('@kbn/object-versioning');
return {
...original,
- getContentManagmentServicesTransforms: (...args: any[]) => {
+ getContentManagementServicesTransforms: (...args: any[]) => {
spy()(...args);
- return original.getContentManagmentServicesTransforms(...args);
+ return original.getContentManagementServicesTransforms(...args);
},
};
});
diff --git a/src/platform/plugins/shared/content_management/server/rpc/procedures/get.test.ts b/src/platform/plugins/shared/content_management/server/rpc/procedures/get.test.ts
index 04d93fddda04f..eaba9e4c3cab7 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/procedures/get.test.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/procedures/get.test.ts
@@ -25,9 +25,9 @@ jest.mock('@kbn/object-versioning', () => {
const original = jest.requireActual('@kbn/object-versioning');
return {
...original,
- getContentManagmentServicesTransforms: (...args: any[]) => {
+ getContentManagementServicesTransforms: (...args: any[]) => {
spy()(...args);
- return original.getContentManagmentServicesTransforms(...args);
+ return original.getContentManagementServicesTransforms(...args);
},
};
});
diff --git a/src/platform/plugins/shared/content_management/server/rpc/procedures/index.ts b/src/platform/plugins/shared/content_management/server/rpc/procedures/index.ts
index 83c2d76d95765..e4d12cb76af7c 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/procedures/index.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/procedures/index.ts
@@ -7,10 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import { Logger } from '@kbn/core/server';
+
import type { ProcedureName } from '../../../common';
import type { RpcService } from '../rpc_service';
import type { Context } from '../types';
-import { procedures } from './all_procedures';
+import { getProcedures } from './all_procedures';
// Type utility to correclty set the type of JS Object.entries()
type Entries = Array<
@@ -19,7 +21,8 @@ type Entries = Array<
}[keyof T]
>;
-export function registerProcedures(rpc: RpcService) {
+export function registerProcedures(rpc: RpcService, logger: Logger) {
+ const procedures = getProcedures(logger);
(Object.entries(procedures) as Entries).forEach(([name, definition]) => {
rpc.register(name, definition);
});
diff --git a/src/platform/plugins/shared/content_management/server/rpc/procedures/msearch.test.ts b/src/platform/plugins/shared/content_management/server/rpc/procedures/msearch.test.ts
index c50ce4307105e..b557b8107736f 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/procedures/msearch.test.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/procedures/msearch.test.ts
@@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import { loggingSystemMock } from '@kbn/core/server/mocks';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
import { MSearchIn, MSearchQuery } from '../../../common';
@@ -15,24 +16,26 @@ import { ContentRegistry } from '../../core/registry';
import { createMockedStorage } from '../../core/mocks';
import { EventBus } from '../../core/event_bus';
import { MSearchService } from '../../core/msearch';
-import { mSearch } from './msearch';
+import { getMSearch } from './msearch';
disableTransformsCache();
const storageContextGetTransforms = jest.fn();
const spy = () => storageContextGetTransforms;
+const mockLoggerFactory = loggingSystemMock.create();
+const mockLogger = mockLoggerFactory.get('mock logger');
jest.mock('@kbn/object-versioning', () => {
const original = jest.requireActual('@kbn/object-versioning');
return {
...original,
- getContentManagmentServicesTransforms: (...args: any[]) => {
+ getContentManagementServicesTransforms: (...args: any[]) => {
spy()(...args);
- return original.getContentManagmentServicesTransforms(...args);
+ return original.getContentManagementServicesTransforms(...args);
},
};
});
-const { fn, schemas } = mSearch;
+const { fn, schemas } = getMSearch(mockLogger);
const inputSchema = schemas?.in;
const outputSchema = schemas?.out;
diff --git a/src/platform/plugins/shared/content_management/server/rpc/procedures/msearch.ts b/src/platform/plugins/shared/content_management/server/rpc/procedures/msearch.ts
index 2de3133ce099c..c1c8accc4e0b7 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/procedures/msearch.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/procedures/msearch.ts
@@ -7,21 +7,26 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import type { Logger } from '@kbn/core/server';
+
import { rpcSchemas } from '../../../common/schemas';
import type { MSearchIn, MSearchOut } from '../../../common';
import type { ProcedureDefinition } from '../rpc_service';
import type { Context } from '../types';
import { getMSearchClientFactory } from '../../content_client';
-export const mSearch: ProcedureDefinition = {
+export const getMSearch = (
+ logger: Logger
+): ProcedureDefinition => ({
schemas: rpcSchemas.mSearch,
fn: async (ctx, { contentTypes, query }) => {
const clientFactory = getMSearchClientFactory({
contentRegistry: ctx.contentRegistry,
mSearchService: ctx.mSearchService,
+ logger,
});
const mSearchClient = clientFactory(ctx);
return mSearchClient.msearch({ contentTypes, query });
},
-};
+});
diff --git a/src/platform/plugins/shared/content_management/server/rpc/procedures/search.test.ts b/src/platform/plugins/shared/content_management/server/rpc/procedures/search.test.ts
index 0c97818f84a22..cd8ae1202d090 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/procedures/search.test.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/procedures/search.test.ts
@@ -26,9 +26,9 @@ jest.mock('@kbn/object-versioning', () => {
const original = jest.requireActual('@kbn/object-versioning');
return {
...original,
- getContentManagmentServicesTransforms: (...args: any[]) => {
+ getContentManagementServicesTransforms: (...args: any[]) => {
spy()(...args);
- return original.getContentManagmentServicesTransforms(...args);
+ return original.getContentManagementServicesTransforms(...args);
},
};
});
diff --git a/src/platform/plugins/shared/content_management/server/rpc/procedures/update.test.ts b/src/platform/plugins/shared/content_management/server/rpc/procedures/update.test.ts
index 6a008e2129e99..ac555f5906db4 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/procedures/update.test.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/procedures/update.test.ts
@@ -25,9 +25,9 @@ jest.mock('@kbn/object-versioning', () => {
const original = jest.requireActual('@kbn/object-versioning');
return {
...original,
- getContentManagmentServicesTransforms: (...args: any[]) => {
+ getContentManagementServicesTransforms: (...args: any[]) => {
spy()(...args);
- return original.getContentManagmentServicesTransforms(...args);
+ return original.getContentManagementServicesTransforms(...args);
},
};
});
diff --git a/src/platform/plugins/shared/content_management/server/rpc/routes/routes.ts b/src/platform/plugins/shared/content_management/server/rpc/routes/routes.ts
index 021fd224c7fe6..12d36273a048a 100644
--- a/src/platform/plugins/shared/content_management/server/rpc/routes/routes.ts
+++ b/src/platform/plugins/shared/content_management/server/rpc/routes/routes.ts
@@ -11,7 +11,7 @@ import { schema } from '@kbn/config-schema';
import type { IRouter } from '@kbn/core/server';
import { LISTING_LIMIT_SETTING, PER_PAGE_SETTING } from '@kbn/saved-objects-settings';
-import { ProcedureName } from '../../../common';
+import { API_ENDPOINT, ProcedureName } from '../../../common';
import type { ContentRegistry } from '../../core';
import { MSearchService } from '../../core/msearch';
@@ -41,7 +41,7 @@ export function initRpcRoutes(
*/
router.post(
{
- path: '/api/content_management/rpc/{name}',
+ path: `${API_ENDPOINT}/{name}`,
security: {
authz: {
enabled: false,
diff --git a/src/platform/plugins/shared/content_management/server/utils/services_transforms_factory.ts b/src/platform/plugins/shared/content_management/server/utils/services_transforms_factory.ts
index 780d44ba465e2..ca34370153cd8 100644
--- a/src/platform/plugins/shared/content_management/server/utils/services_transforms_factory.ts
+++ b/src/platform/plugins/shared/content_management/server/utils/services_transforms_factory.ts
@@ -12,7 +12,7 @@ import type { ObjectMigrationDefinition } from '@kbn/object-versioning';
import type { ContentManagementServiceDefinitionVersioned, Version } from '@kbn/object-versioning';
import {
compileServiceDefinitions,
- getContentManagmentServicesTransforms,
+ getContentManagementServicesTransforms,
} from '@kbn/object-versioning';
import type { StorageContextGetTransformFn } from '../core';
@@ -31,13 +31,13 @@ const compiledCache = new LRUCache
@@ -48,7 +48,7 @@ export const getServiceObjectTransformFactory =
const compiledFromCache = compiledCache.get(contentTypeId);
if (compiledFromCache) {
- return getContentManagmentServicesTransforms(
+ return getContentManagementServicesTransforms(
definitions,
requestVersion,
compiledFromCache
@@ -62,5 +62,5 @@ export const getServiceObjectTransformFactory =
compiledCache.set(contentTypeId, compiled);
}
- return getContentManagmentServicesTransforms(definitions, requestVersion, compiled);
+ return getContentManagementServicesTransforms(definitions, requestVersion, compiled);
};
diff --git a/src/platform/plugins/shared/content_management/server/utils/utils.ts b/src/platform/plugins/shared/content_management/server/utils/utils.ts
index 8012592048fb9..69e564a77f470 100644
--- a/src/platform/plugins/shared/content_management/server/utils/utils.ts
+++ b/src/platform/plugins/shared/content_management/server/utils/utils.ts
@@ -8,6 +8,7 @@
*/
import { Type, ValidationError } from '@kbn/config-schema';
+import { KibanaRequest } from '@kbn/core/server';
import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import { validateVersion } from '@kbn/object-versioning/lib/utils';
import type { Version } from '@kbn/object-versioning';
@@ -47,10 +48,12 @@ const validateRequestVersion = (
};
export const getStorageContext = ({
+ request,
contentTypeId,
version: _version,
ctx: { contentRegistry, requestHandlerContext, getTransformsFactory },
}: {
+ request: KibanaRequest;
contentTypeId: string;
version?: number;
ctx: {
@@ -62,6 +65,7 @@ export const getStorageContext = ({
const contentDefinition = contentRegistry.getDefinition(contentTypeId);
const version = validateRequestVersion(_version, contentDefinition.version.latest);
const storageContext: StorageContext = {
+ request,
requestHandlerContext,
version: {
request: version,
diff --git a/src/platform/plugins/shared/content_management/tsconfig.json b/src/platform/plugins/shared/content_management/tsconfig.json
index e02852e8fad3d..0cf799198a7b6 100644
--- a/src/platform/plugins/shared/content_management/tsconfig.json
+++ b/src/platform/plugins/shared/content_management/tsconfig.json
@@ -19,6 +19,7 @@
"@kbn/content-management-favorites-server",
"@kbn/usage-collection-plugin",
"@kbn/object-versioning-utils",
+ "@kbn/core-http-router-server-mocks",
],
"exclude": [
"target/**/*",
diff --git a/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts b/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts
index 94c4e2b6b79fe..d30bb069b1f62 100644
--- a/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts
+++ b/src/platform/plugins/shared/dashboard/server/content_management/dashboard_storage.ts
@@ -210,6 +210,7 @@ export class DashboardStorage {
DashboardGetOut,
DashboardGetOut
>(
+ // @ts-expect-error - need to fix response.item type here
response,
undefined, // do not override version
{ validate: false } // validation is done above
@@ -296,6 +297,7 @@ export class DashboardStorage {
const { value, error: resultError } = transforms.create.out.result.down<
CreateResult
>(
+ // @ts-expect-error - need to fix item type here
{ item },
undefined, // do not override version
{ validate: false } // validation is done above
@@ -380,6 +382,7 @@ export class DashboardStorage {
DashboardUpdateOut,
DashboardUpdateOut
>(
+ // @ts-expect-error - need to fix item type here
{ item },
undefined, // do not override version
{ validate: false } // validation is done above
@@ -458,6 +461,7 @@ export class DashboardStorage {
DashboardSearchOut,
DashboardSearchOut
>(
+ // @ts-expect-error - need to fix response.hits type here
response,
undefined, // do not override version
{ validate: false } // validation is done above
diff --git a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx
index 147a8a4c92a08..a97f2709b7fe8 100644
--- a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx
+++ b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx
@@ -15,6 +15,7 @@ import { v4 as generateId } from 'uuid';
import { PhaseTracker } from './phase_tracker';
import { getReactEmbeddableFactory } from './react_embeddable_registry';
import { DefaultEmbeddableApi, EmbeddableApiRegistration } from './types';
+import { getTransforms, hasTransforms } from '../transforms_registry';
/**
* Renders a component from the React Embeddable registry into a Presentation Panel.
@@ -63,6 +64,7 @@ export const EmbeddableRenderer = <
const buildEmbeddable = async () => {
const factory = await getReactEmbeddableFactory(type);
+ const transforms = hasTransforms(type) ? await getTransforms(type) : null;
const finalizeApi = (
apiRegistration: EmbeddableApiRegistration
@@ -84,8 +86,14 @@ export const EmbeddableRenderer = <
const initialState = parentApi.getSerializedStateForChild(uuid) ?? {
rawState: {} as SerializedState,
};
+ const transformedInitialState =
+ transforms?.transformOut?.(initialState.rawState, initialState.references) ??
+ initialState.rawState;
const { api, Component } = await factory.buildEmbeddable({
- initialState,
+ initialState: {
+ ...initialState,
+ rawState: transformedInitialState,
+ },
finalizeApi,
uuid,
parentApi,
diff --git a/src/platform/plugins/shared/visualizations/public/content_management/visualization_client.ts b/src/platform/plugins/shared/visualizations/public/content_management/visualization_client.ts
index 220ba191fbc6c..17d6f32137822 100644
--- a/src/platform/plugins/shared/visualizations/public/content_management/visualization_client.ts
+++ b/src/platform/plugins/shared/visualizations/public/content_management/visualization_client.ts
@@ -69,6 +69,9 @@ const deleteVisualization = async (id: string) => {
const search = async (query: SearchQuery = {}, options?: VisualizationSearchQuery) => {
if (options && options.types && options.types.length > 1) {
const { types } = options;
+ // TODO: Fix types - This assumes the return types are all VisualizationSavedObject but that is false
+ // these can be any content type provided. There should be a separate mSearch method that returns
+ // saved objects with generic shared attributes (i.e. `title`, `description`) needed to list items.
return getContentManagement().client.mSearch({
contentTypes: types.map((type) => ({ contentTypeId: type })),
query,
diff --git a/src/platform/plugins/shared/visualizations/public/index.ts b/src/platform/plugins/shared/visualizations/public/index.ts
index fbffa8c09fa90..eb9174a03e674 100644
--- a/src/platform/plugins/shared/visualizations/public/index.ts
+++ b/src/platform/plugins/shared/visualizations/public/index.ts
@@ -36,6 +36,7 @@ export type {
Schema,
ISchemas,
VisualizationClient,
+ BasicVisualizationClient,
SerializableAttributes,
} from './vis_types';
export type { VisualizeEditorInput } from './embeddable/types';
diff --git a/src/platform/plugins/shared/visualizations/public/utils/saved_objects_utils/update_basic_attributes.ts b/src/platform/plugins/shared/visualizations/public/utils/saved_objects_utils/update_basic_attributes.ts
index 108bba58f7252..ba1af19b30306 100644
--- a/src/platform/plugins/shared/visualizations/public/utils/saved_objects_utils/update_basic_attributes.ts
+++ b/src/platform/plugins/shared/visualizations/public/utils/saved_objects_utils/update_basic_attributes.ts
@@ -7,32 +7,34 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import { HttpStart } from '@kbn/core/public';
import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { OverlayStart } from '@kbn/core-overlays-browser';
-
import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
+
import { extractReferences } from '../saved_visualization_references';
import { visualizationsClient } from '../../content_management';
-import { TypesStart } from '../../vis_types';
+import { BasicVisualizationClient, TypesStart } from '../../vis_types';
interface UpdateBasicSoAttributesDependencies {
savedObjectsTagging?: SavedObjectsTaggingApi;
overlays: OverlayStart;
typesService: TypesStart;
contentManagement: ContentManagementPublicStart;
+ http: HttpStart;
}
function getClientForType(
type: string,
typesService: TypesStart,
- contentManagement: ContentManagementPublicStart
-) {
+ contentManagement: ContentManagementPublicStart,
+ http: HttpStart
+): BasicVisualizationClient {
const visAliases = typesService.getAliases();
- return (
- visAliases
- .find((v) => v.appExtensions?.visualizations.docTypes.includes(type))
- ?.appExtensions?.visualizations.client(contentManagement) || visualizationsClient
- );
+ return (visAliases
+ .find((v) => v.appExtensions?.visualizations.docTypes.includes(type))
+ ?.appExtensions?.visualizations.client(contentManagement, http) ||
+ visualizationsClient) as BasicVisualizationClient;
}
function getAdditionalOptionsForUpdate(
@@ -58,7 +60,12 @@ export const updateBasicSoAttributes = async (
},
dependencies: UpdateBasicSoAttributesDependencies
) => {
- const client = getClientForType(type, dependencies.typesService, dependencies.contentManagement);
+ const client = getClientForType(
+ type,
+ dependencies.typesService,
+ dependencies.contentManagement,
+ dependencies.http
+ );
const so = await client.get(soId);
const extractedReferences = extractReferences({
diff --git a/src/platform/plugins/shared/visualizations/public/vis_types/index.ts b/src/platform/plugins/shared/visualizations/public/vis_types/index.ts
index a006d621b2405..7931b2c206361 100644
--- a/src/platform/plugins/shared/visualizations/public/vis_types/index.ts
+++ b/src/platform/plugins/shared/visualizations/public/vis_types/index.ts
@@ -12,4 +12,8 @@ export { Schemas } from './schemas';
export { VisGroups } from './vis_groups_enum';
export { BaseVisType } from './base_vis_type';
export type { VisTypeDefinition, ISchemas, Schema } from './types';
-export type { VisualizationClient, SerializableAttributes } from './vis_type_alias_registry';
+export type {
+ VisualizationClient,
+ BasicVisualizationClient,
+ SerializableAttributes,
+} from './vis_type_alias_registry';
diff --git a/src/platform/plugins/shared/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/platform/plugins/shared/visualizations/public/vis_types/vis_type_alias_registry.ts
index bffd3150f515b..2703013268ee9 100644
--- a/src/platform/plugins/shared/visualizations/public/vis_types/vis_type_alias_registry.ts
+++ b/src/platform/plugins/shared/visualizations/public/vis_types/vis_type_alias_registry.ts
@@ -14,6 +14,7 @@ import type {
SavedObjectCreateOptions,
SavedObjectUpdateOptions,
} from '@kbn/content-management-utils';
+import { HttpStart } from '@kbn/data-view-editor-plugin/public/shared_imports';
import { BaseVisType } from './base_vis_type';
import { VisualizationSavedObject } from '../../common';
@@ -75,6 +76,17 @@ export interface VisualizationClient<
) => Promise['SearchOut']>;
}
+/**
+ * A slim visualization client used but the vis plugin to save basic attributes
+ *
+ * TODO cleanup the vis-type client code.
+ * All viz pass this client require type overrides to adopt VisualizationClient
+ */
+export type BasicVisualizationClient<
+ ContentType extends string = string,
+ Attr extends SerializableAttributes = SerializableAttributes
+> = Pick, 'get' | 'update'>;
+
export interface VisualizationsAppExtension {
docTypes: string[];
searchFields?: string[];
@@ -83,7 +95,10 @@ export interface VisualizationsAppExtension {
update?: { overwrite?: boolean; [otherOption: string]: unknown };
create?: { [otherOption: string]: unknown };
};
- client: (contentManagement: ContentManagementPublicStart) => VisualizationClient;
+ client: (
+ contentManagement: ContentManagementPublicStart,
+ http: HttpStart
+ ) => BasicVisualizationClient;
toListItem: (savedObject: VisualizationSavedObject) => VisualizationListItem;
}
diff --git a/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_listing.tsx
index 48e8a2ad54872..4df2638363932 100644
--- a/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_listing.tsx
+++ b/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_listing.tsx
@@ -134,6 +134,7 @@ const useTableListViewProps = (
toastNotifications,
visualizeCapabilities,
contentManagement,
+ http,
...startServices
},
} = useKibana();
@@ -212,12 +213,13 @@ const useTableListViewProps = (
savedObjectsTagging,
typesService: getTypes(),
contentManagement,
+ http,
...startServices,
}
);
}
},
- [savedObjectsTagging, contentManagement, startServices]
+ [savedObjectsTagging, contentManagement, http, startServices]
);
const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo(
diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx
index 200b44bf6eebf..cda86c89b6e95 100644
--- a/x-pack/examples/embedded_lens_example/public/app.tsx
+++ b/x-pack/examples/embedded_lens_example/public/app.tsx
@@ -28,6 +28,7 @@ import type {
} from '@kbn/lens-plugin/public';
import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
+import { LENS_ITEM_LATEST_VERSION } from '@kbn/lens-plugin/common/constants';
import type { StartDependencies } from './plugin';
// Generate a Lens state based on some app-specific input parameters.
@@ -82,6 +83,7 @@ function getLensAttributes(
return {
visualizationType: 'lnsXY',
title: 'Prefilled from example app',
+ version: LENS_ITEM_LATEST_VERSION,
references: [
{
id: dataView.id!,
diff --git a/x-pack/examples/testing_embedded_lens/public/app.tsx b/x-pack/examples/testing_embedded_lens/public/app.tsx
index 56aaf72623667..54f4c22e44109 100644
--- a/x-pack/examples/testing_embedded_lens/public/app.tsx
+++ b/x-pack/examples/testing_embedded_lens/public/app.tsx
@@ -22,7 +22,7 @@ import {
} from '@elastic/eui';
import type { CoreStart } from '@kbn/core/public';
import useDebounce from 'react-use/lib/useDebounce';
-import { DOCUMENT_FIELD_NAME } from '@kbn/lens-plugin/common/constants';
+import { DOCUMENT_FIELD_NAME, LENS_ITEM_LATEST_VERSION } from '@kbn/lens-plugin/common/constants';
import type { DataView } from '@kbn/data-views-plugin/public';
import type {
TypedLensByValueInput,
@@ -150,6 +150,7 @@ function getBaseAttributes(
const finalDataLayer = dataLayer ?? getDataLayer(finalType, fields[finalType]);
return {
title: 'Prefilled from example app',
+ version: LENS_ITEM_LATEST_VERSION,
references: [
{
id: defaultIndexPattern.id!,
diff --git a/x-pack/examples/third_party_lens_navigation_prompt/public/plugin.ts b/x-pack/examples/third_party_lens_navigation_prompt/public/plugin.ts
index f38d8d9e3e8bf..2bad7ca615a5e 100644
--- a/x-pack/examples/third_party_lens_navigation_prompt/public/plugin.ts
+++ b/x-pack/examples/third_party_lens_navigation_prompt/public/plugin.ts
@@ -15,6 +15,7 @@ import {
} from '@kbn/lens-plugin/public';
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
import { TypedLensByValueInput, PersistedIndexPatternLayer } from '@kbn/lens-plugin/public';
+import { LENS_ITEM_LATEST_VERSION } from '@kbn/lens-plugin/common/constants';
import image from './image.png';
export interface SetupDependencies {
@@ -55,6 +56,7 @@ function getLensAttributes(defaultDataView: DataView): TypedLensByValueInput['at
};
return {
+ version: LENS_ITEM_LATEST_VERSION,
visualizationType: 'lnsDatatable',
title: 'Prefilled from example app',
references: [
diff --git a/x-pack/examples/third_party_vis_lens_example/public/plugin.ts b/x-pack/examples/third_party_vis_lens_example/public/plugin.ts
index a05c76e22c9dc..9aa5ced6c7ec6 100644
--- a/x-pack/examples/third_party_vis_lens_example/public/plugin.ts
+++ b/x-pack/examples/third_party_vis_lens_example/public/plugin.ts
@@ -12,6 +12,7 @@ import { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/pub
import { LensPublicSetup, LensPublicStart } from '@kbn/lens-plugin/public';
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
import { TypedLensByValueInput, PersistedIndexPatternLayer } from '@kbn/lens-plugin/public';
+import { LENS_ITEM_LATEST_VERSION } from '@kbn/lens-plugin/common/constants';
import { getRotatingNumberRenderer, rotatingNumberFunction } from './expression';
import { getRotatingNumberVisualization } from './visualization';
import { RotatingNumberState } from '../common/types';
@@ -51,6 +52,7 @@ function getLensAttributes(defaultDataView: DataView): TypedLensByValueInput['at
};
return {
+ version: LENS_ITEM_LATEST_VERSION,
visualizationType: 'rotatingNumber',
title: 'Prefilled from example app',
references: [
diff --git a/x-pack/platform/plugins/shared/lens/common/constants.ts b/x-pack/platform/plugins/shared/lens/common/constants.ts
index 91e47eea2b97b..cc826b6bf1c0a 100644
--- a/x-pack/platform/plugins/shared/lens/common/constants.ts
+++ b/x-pack/platform/plugins/shared/lens/common/constants.ts
@@ -9,6 +9,14 @@ import rison from '@kbn/rison';
import type { RefreshInterval, TimeRange } from '@kbn/data-plugin/common';
import type { Filter } from '@kbn/es-query';
+import { LENS_ITEM_VERSION_V1 } from './content_management/constants';
+
+export {
+ LENS_ITEM_VERSION_V1,
+ LENS_ITEM_LATEST_VERSION,
+ LENS_CONTENT_TYPE,
+} from './content_management/constants';
+
export const PLUGIN_ID = 'lens';
export const APP_ID = PLUGIN_ID;
export const DOC_TYPE = 'lens';
@@ -20,6 +28,12 @@ export const LENS_EDIT_BY_VALUE = 'edit_by_value';
export const LENS_ICON = 'lensApp';
export const STAGE_ID = 'production';
+export const LENS_API_CONTENT_MANAGEMENT_VERSION = LENS_ITEM_VERSION_V1;
+export const LENS_API_VERSION = '1';
+export const LENS_API_ACCESS = 'internal';
+export const LENS_API_PATH = '/api/lens';
+export const LENS_VIS_API_PATH = `${LENS_API_PATH}/visualizations`;
+
export const INDEX_PATTERN_TYPE = 'index-pattern';
export const PieChartTypes = {
diff --git a/x-pack/platform/plugins/shared/lens/common/content_management/constants.ts b/x-pack/platform/plugins/shared/lens/common/content_management/constants.ts
index eecfbceae8325..5c9a99b1484ed 100644
--- a/x-pack/platform/plugins/shared/lens/common/content_management/constants.ts
+++ b/x-pack/platform/plugins/shared/lens/common/content_management/constants.ts
@@ -5,6 +5,20 @@
* 2.0.
*/
-export const LATEST_VERSION = 1;
+/**
+ * Lens CM Item Version `v1`
+ */
+export const LENS_ITEM_VERSION_V1 = 1 as const;
+export type LENS_ITEM_VERSION_V1 = typeof LENS_ITEM_VERSION_V1;
+
+/**
+ * Latest Lens CM Item Version
+ */
+export const LENS_ITEM_LATEST_VERSION = LENS_ITEM_VERSION_V1;
+export type LENS_ITEM_LATEST_VERSION = typeof LENS_ITEM_LATEST_VERSION;
-export const CONTENT_ID = 'lens';
+/**
+ * Lens CM Item content type
+ */
+export const LENS_CONTENT_TYPE = 'lens';
+export type LENS_CONTENT_TYPE = typeof LENS_CONTENT_TYPE;
diff --git a/x-pack/platform/plugins/shared/lens/common/content_management/index.ts b/x-pack/platform/plugins/shared/lens/common/content_management/index.ts
deleted file mode 100644
index f3363af80ab02..0000000000000
--- a/x-pack/platform/plugins/shared/lens/common/content_management/index.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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 { LensContentType } from './types';
-
-export type {
- LensSavedObject,
- PartialLensSavedObject,
- LensSavedObjectAttributes,
- LensGetIn,
- LensGetOut,
- LensCreateIn,
- LensCreateOut,
- CreateOptions,
- LensUpdateIn,
- LensUpdateOut,
- UpdateOptions,
- LensDeleteIn,
- LensDeleteOut,
- LensSearchIn,
- LensSearchOut,
- LensSearchQuery,
- LensCrudTypes,
-} from './latest';
-
-export type * as LensV1 from './v1';
diff --git a/x-pack/platform/plugins/shared/lens/common/content_management/v1/index.ts b/x-pack/platform/plugins/shared/lens/common/content_management/v1/index.ts
deleted file mode 100644
index 9e07159b8a5f5..0000000000000
--- a/x-pack/platform/plugins/shared/lens/common/content_management/v1/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 {
- LensSavedObject,
- PartialLensSavedObject,
- LensSavedObjectAttributes,
- LensGetIn,
- LensGetOut,
- LensCreateIn,
- LensCreateOut,
- CreateOptions,
- LensUpdateIn,
- LensUpdateOut,
- UpdateOptions,
- LensDeleteIn,
- LensDeleteOut,
- LensSearchIn,
- LensSearchOut,
- LensSearchQuery,
- LensCrudTypes,
-} from './types';
diff --git a/x-pack/platform/plugins/shared/lens/common/content_management/v1/types.ts b/x-pack/platform/plugins/shared/lens/common/content_management/v1/types.ts
deleted file mode 100644
index 17de8624ccb94..0000000000000
--- a/x-pack/platform/plugins/shared/lens/common/content_management/v1/types.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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 { ContentManagementCrudTypes } from '@kbn/content-management-utils';
-import type { Reference } from '@kbn/content-management-utils';
-import type { UpdateIn } from '@kbn/content-management-plugin/common';
-
-import type { LensContentType } from '../types';
-
-export interface CreateOptions {
- /** If a document with the given `id` already exists, overwrite it's contents (default=false). */
- overwrite?: boolean;
- /** Array of referenced saved objects. */
- references?: Reference[];
-}
-
-export interface UpdateOptions {
- /** Array of referenced saved objects. */
- references?: Reference[];
-}
-
-export interface LensSearchQuery {
- searchFields?: string[];
-}
-
-// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
-export type LensSavedObjectAttributes = {
- title: string;
- description?: string;
- visualizationType: string | null;
- state?: unknown;
-};
-
-// Need to handle update in Lens in a bit different way
-export type LensCrudTypes = Omit<
- ContentManagementCrudTypes<
- LensContentType,
- LensSavedObjectAttributes,
- CreateOptions,
- UpdateOptions,
- LensSearchQuery
- >,
- 'UpdateIn'
-> & { UpdateIn: UpdateIn };
-
-export type LensSavedObject = LensCrudTypes['Item'];
-export type PartialLensSavedObject = LensCrudTypes['PartialItem'];
-
-// ----------- GET --------------
-export type LensGetIn = LensCrudTypes['GetIn'];
-export type LensGetOut = LensCrudTypes['GetOut'];
-
-// ----------- CREATE --------------
-export type LensCreateIn = LensCrudTypes['CreateIn'];
-export type LensCreateOut = LensCrudTypes['CreateOut'];
-
-// ----------- UPDATE --------------
-export type LensUpdateIn = LensCrudTypes['UpdateIn'];
-export type LensUpdateOut = LensCrudTypes['UpdateOut'];
-
-// ----------- DELETE --------------
-export type LensDeleteIn = LensCrudTypes['DeleteIn'];
-export type LensDeleteOut = LensCrudTypes['DeleteOut'];
-
-// ----------- SEARCH --------------
-export type LensSearchIn = LensCrudTypes['SearchIn'];
-export type LensSearchOut = LensCrudTypes['SearchOut'];
diff --git a/x-pack/platform/plugins/shared/lens/common/transforms/config_builder_stub.ts b/x-pack/platform/plugins/shared/lens/common/transforms/config_builder_stub.ts
new file mode 100644
index 0000000000000..a1c79389cd4b3
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/common/transforms/config_builder_stub.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.
+ */
+
+import type { LensAPIConfig, LensItem } from '../../server/content_management';
+
+export function isNewApiFormat(config: unknown): config is LensAPIConfig {
+ return (config as LensAPIConfig)?.state?.isNewApiFormat;
+}
+
+export const ConfigBuilderStub = {
+ /**
+ * @returns Lens item
+ */
+ in(config: LensAPIConfig): LensItem {
+ return config;
+ },
+
+ /**
+ * @returns Lens API config
+ */
+ out(item: LensItem): LensAPIConfig {
+ return {
+ ...item,
+ state: {
+ ...item.state,
+ isNewApiFormat: true,
+ },
+ };
+ },
+};
diff --git a/x-pack/platform/plugins/shared/lens/common/transforms/index.ts b/x-pack/platform/plugins/shared/lens/common/transforms/index.ts
new file mode 100644
index 0000000000000..1ff8685ea5ca3
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/common/transforms/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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { ConfigBuilderStub } from './config_builder_stub';
+export type * from './types';
diff --git a/x-pack/platform/plugins/shared/lens/common/transforms/transforms.ts b/x-pack/platform/plugins/shared/lens/common/transforms/transforms.ts
new file mode 100644
index 0000000000000..6b978f1f1bd4d
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/common/transforms/transforms.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { EmbeddableTransforms } from '@kbn/embeddable-plugin/common';
+
+import type { LensSerializedState } from '../../public';
+import {
+ LENS_ITEM_VERSION as LENS_ITEM_VERSION_V1,
+ transformToV1LensItemAttributes,
+} from '../content_management/v1';
+
+export const lensTransforms = {
+ transformOut(state: LensSerializedState): LensSerializedState {
+ // skip by-ref Lens panels
+ if (!state.attributes) return state;
+
+ const version = state.attributes.version ?? 0;
+ const newState = { ...state };
+
+ if (version < LENS_ITEM_VERSION_V1) {
+ newState.attributes = transformToV1LensItemAttributes({
+ ...state.attributes,
+ description: state.attributes.description ?? '',
+ }) as LensSerializedState['attributes'];
+ }
+
+ return newState;
+ },
+} satisfies EmbeddableTransforms;
diff --git a/x-pack/platform/plugins/shared/lens/common/transforms/types.ts b/x-pack/platform/plugins/shared/lens/common/transforms/types.ts
new file mode 100644
index 0000000000000..4e453a092ee41
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/common/transforms/types.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 { LensItem as LensItemV1, LensAttributesV0 } from '../content_management/v1';
+import type { LensItem } from '../content_management';
+
+/**
+ * Any of the versioned Lens Item
+ */
+export type VersionLensItem = LensItem | LensItemV1;
+
+/**
+ * Any of the versioned or unversioned Lens Item
+ */
+export type UnknownLensItem = LensAttributesV0 | VersionLensItem;
diff --git a/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_document_equality.test.ts b/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_document_equality.test.ts
index 9b7bbb9432da8..f4bc994ad6f7e 100644
--- a/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_document_equality.test.ts
+++ b/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_document_equality.test.ts
@@ -15,6 +15,7 @@ import {
Visualization,
VisualizationMap,
} from '../types';
+import { LENS_ITEM_LATEST_VERSION } from '../../common/constants';
const visualizationType = 'lnsSomeVis';
@@ -47,6 +48,7 @@ const defaultDoc: LensDocument = {
type: 'index-pattern',
},
],
+ version: LENS_ITEM_LATEST_VERSION,
};
describe('lens document equality', () => {
diff --git a/x-pack/platform/plugins/shared/lens/public/app_plugin/mounter.tsx b/x-pack/platform/plugins/shared/lens/public/app_plugin/mounter.tsx
index a3b3e62ee1c9f..a302d18380fd1 100644
--- a/x-pack/platform/plugins/shared/lens/public/app_plugin/mounter.tsx
+++ b/x-pack/platform/plugins/shared/lens/public/app_plugin/mounter.tsx
@@ -118,7 +118,7 @@ export async function getLensServices(
attributeService,
eventAnnotationService,
uiActions: startDependencies.uiActions,
- lensDocumentService: new LensDocumentService(startDependencies.contentManagement),
+ lensDocumentService: new LensDocumentService(coreStart.http),
presentationUtil: startDependencies.presentationUtil,
dataViewEditor: startDependencies.dataViewEditor,
dataViewFieldEditor: startDependencies.dataViewFieldEditor,
diff --git a/x-pack/platform/plugins/shared/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx b/x-pack/platform/plugins/shared/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx
index 3564016e1e3dd..efe3a46f125d7 100644
--- a/x-pack/platform/plugins/shared/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx
+++ b/x-pack/platform/plugins/shared/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx
@@ -134,7 +134,7 @@ export async function getEditLensConfiguration(
const lensServices = await getLensServices(
coreStart,
startDependencies,
- getLensAttributeService(startDependencies)
+ getLensAttributeService(coreStart.http)
);
return ({
@@ -172,7 +172,7 @@ export async function getEditLensConfiguration(
*/
const saveByRef = useCallback(
async (attrs: LensDocument) => {
- const lensDocumentService = new LensDocumentService(lensServices.contentManagement);
+ const lensDocumentService = new LensDocumentService(lensServices.http);
await lensDocumentService.save({
...attrs,
savedObjectId,
diff --git a/x-pack/platform/plugins/shared/lens/public/app_plugin/shared/saved_modal_lazy.tsx b/x-pack/platform/plugins/shared/lens/public/app_plugin/shared/saved_modal_lazy.tsx
index 8722175e1f19d..97069c16415a1 100644
--- a/x-pack/platform/plugins/shared/lens/public/app_plugin/shared/saved_modal_lazy.tsx
+++ b/x-pack/platform/plugins/shared/lens/public/app_plugin/shared/saved_modal_lazy.tsx
@@ -45,7 +45,7 @@ export function getSaveModalComponent(
const lensServicesT = await getLensServices(
coreStart,
startDependencies,
- getLensAttributeService(startDependencies)
+ getLensAttributeService(coreStart.http)
);
setLensServices(lensServicesT);
diff --git a/x-pack/platform/plugins/shared/lens/public/chart_info_api.test.ts b/x-pack/platform/plugins/shared/lens/public/chart_info_api.test.ts
index f647e2289c5bf..8e8d8674865ae 100644
--- a/x-pack/platform/plugins/shared/lens/public/chart_info_api.test.ts
+++ b/x-pack/platform/plugins/shared/lens/public/chart_info_api.test.ts
@@ -9,6 +9,7 @@ import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { createChartInfoApi } from './chart_info_api';
import { LensDocument } from './persistence';
import { DatasourceMap, VisualizationMap } from './types';
+import { LENS_ITEM_LATEST_VERSION } from '../common/constants';
const mockGetVisualizationInfo = jest.fn().mockReturnValue({
layers: [
@@ -70,6 +71,7 @@ describe('createChartInfoApi', () => {
query: '',
},
references: [],
+ version: LENS_ITEM_LATEST_VERSION,
} as LensDocument;
const chartInfo = await chartInfoApi.getChartInfo(vis);
diff --git a/x-pack/platform/plugins/shared/lens/public/lens_attribute_service.ts b/x-pack/platform/plugins/shared/lens/public/lens_attribute_service.ts
index 4ea62078bbd69..f66e106c7ae43 100644
--- a/x-pack/platform/plugins/shared/lens/public/lens_attribute_service.ts
+++ b/x-pack/platform/plugins/shared/lens/public/lens_attribute_service.ts
@@ -10,8 +10,8 @@ import { OnSaveProps } from '@kbn/saved-objects-plugin/public';
import { SavedObjectCommon } from '@kbn/saved-objects-finder-plugin/common';
import { noop } from 'lodash';
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
-import type { LensPluginStartDependencies } from './plugin';
-import type { LensSavedObjectAttributes as LensSavedObjectAttributesWithoutReferences } from '../common/content_management';
+import { HttpStart } from '@kbn/core/public';
+import type { LensAttributes } from '../server/content_management';
import { extract, inject } from '../common/embeddable_factory';
import { LensDocumentService } from './persistence';
import { DOC_TYPE } from '../common/constants';
@@ -32,7 +32,7 @@ export interface LensAttributesService {
managed: boolean;
}>;
saveToLibrary: (
- attributes: LensSavedObjectAttributesWithoutReferences,
+ attributes: LensAttributes,
references: Reference[],
savedObjectId?: string
) => Promise;
@@ -48,7 +48,7 @@ export interface LensAttributesService {
}
export const savedObjectToEmbeddableAttributes = (
- savedObject: SavedObjectCommon
+ savedObject: SavedObjectCommon
): LensSavedObjectAttributes => {
return {
...savedObject.attributes,
@@ -57,10 +57,8 @@ export const savedObjectToEmbeddableAttributes = (
};
};
-export function getLensAttributeService({
- contentManagement,
-}: LensPluginStartDependencies): LensAttributesService {
- const lensDocumentService = new LensDocumentService(contentManagement);
+export function getLensAttributeService(http: HttpStart): LensAttributesService {
+ const lensDocumentService = new LensDocumentService(http);
return {
loadFromLibrary: async (
@@ -70,11 +68,11 @@ export function getLensAttributeService({
sharingSavedObjectProps: SharingSavedObjectProps;
managed: boolean;
}> => {
- const { meta, item } = await lensDocumentService.load(savedObjectId);
+ const { item, meta } = await lensDocumentService.load(savedObjectId);
return {
attributes: {
- ...item.attributes,
- state: item.attributes.state as LensSavedObjectAttributes['state'],
+ ...item,
+ state: item.state as LensSavedObjectAttributes['state'],
references: item.references,
},
sharingSavedObjectProps: {
@@ -83,11 +81,11 @@ export function getLensAttributeService({
aliasPurpose: meta.aliasPurpose,
sourceId: item.id,
},
- managed: Boolean(item.managed),
+ managed: Boolean(meta.managed),
};
},
saveToLibrary: async (
- attributes: LensSavedObjectAttributesWithoutReferences,
+ attributes: LensAttributes,
references: Reference[],
savedObjectId?: string
) => {
diff --git a/x-pack/platform/plugins/shared/lens/public/mocks/services_mock.tsx b/x-pack/platform/plugins/shared/lens/public/mocks/services_mock.tsx
index 72a83c1a906a3..5a6bf737c3477 100644
--- a/x-pack/platform/plugins/shared/lens/public/mocks/services_mock.tsx
+++ b/x-pack/platform/plugins/shared/lens/public/mocks/services_mock.tsx
@@ -33,6 +33,7 @@ import { getLensInspectorService } from '../lens_inspector_service';
import { LensDocument, LensDocumentService } from '../persistence';
import { LensAttributesService } from '../lens_attribute_service';
import { mockDatasourceStates } from './store_mocks';
+import { LENS_ITEM_LATEST_VERSION } from '../../common/constants';
const startMock = coreMock.createStart();
@@ -47,6 +48,7 @@ export const defaultDoc: LensDocument = {
visualization: {},
},
references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
+ version: LENS_ITEM_LATEST_VERSION,
};
export const exactMatchDoc = {
diff --git a/x-pack/platform/plugins/shared/lens/public/persistence/basic_lens_client.ts b/x-pack/platform/plugins/shared/lens/public/persistence/basic_lens_client.ts
new file mode 100644
index 0000000000000..d0a8e15179e94
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/public/persistence/basic_lens_client.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 { HttpStart } from '@kbn/core/public';
+import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
+import type {
+ SerializableAttributes,
+ BasicVisualizationClient,
+} from '@kbn/visualizations-plugin/public';
+
+import type { LensAttributes } from '../../server/content_management';
+import { LensClient } from './lens_client';
+import { getLensSOFromResponse } from './utils';
+
+/**
+ * This is a wrapper client used only to update basic attributes from the vis plugin
+ */
+export function getLensBasicClient(
+ cm: ContentManagementPublicStart,
+ http: HttpStart
+): BasicVisualizationClient<'lens', Attr> {
+ const lensClient = new LensClient(http);
+
+ return {
+ get: async (id: string) => {
+ const result = await lensClient.get(id);
+ const lensSavedObject = getLensSOFromResponse(result);
+
+ return {
+ item: {
+ ...lensSavedObject,
+ // TODO: Fix this attributes type when config builder changes are applied
+ attributes: lensSavedObject.attributes as unknown as Attr,
+ },
+ meta: {
+ outcome: result.meta.outcome,
+ },
+ } satisfies Awaited['get']>>;
+ },
+
+ update: async ({ id, options = {}, data = {} }) => {
+ const result = await lensClient.update(
+ id,
+ data as unknown as LensAttributes,
+ options.references ?? []
+ );
+
+ const lensSavedObject = getLensSOFromResponse(result);
+
+ return {
+ item: {
+ ...lensSavedObject,
+ // TODO fix this attributes type when config builder changes are applied
+ attributes: lensSavedObject.attributes as unknown as Attr,
+ },
+ } satisfies Awaited['update']>>;
+ },
+ };
+}
diff --git a/x-pack/platform/plugins/shared/lens/public/persistence/lens_client.ts b/x-pack/platform/plugins/shared/lens/public/persistence/lens_client.ts
index 7517c26c87a10..9de419299a738 100644
--- a/x-pack/platform/plugins/shared/lens/public/persistence/lens_client.ts
+++ b/x-pack/platform/plugins/shared/lens/public/persistence/lens_client.ts
@@ -5,77 +5,141 @@
* 2.0.
*/
-import type { SearchQuery } from '@kbn/content-management-plugin/common';
-import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
-import type {
- SerializableAttributes,
- VisualizationClient,
-} from '@kbn/visualizations-plugin/public';
-import { DOC_TYPE } from '../../common/constants';
+import { HttpStart } from '@kbn/core/public';
+import type { Reference } from '@kbn/content-management-utils';
+
+import { omit } from 'lodash';
+import { LENS_API_VERSION, LENS_VIS_API_PATH } from '../../common/constants';
+import type { LensAttributes, LensItem } from '../../server/content_management';
+import { ConfigBuilderStub } from '../../common/transforms';
import {
- LensCreateIn,
- LensCreateOut,
- LensDeleteIn,
- LensDeleteOut,
- LensGetIn,
- LensGetOut,
- LensSearchIn,
- LensSearchOut,
- LensSearchQuery,
- LensUpdateIn,
- LensUpdateOut,
-} from '../../common/content_management';
-
-export function getLensClient(
- cm: ContentManagementPublicStart
-): VisualizationClient<'lens', Attr> {
- const get = async (id: string) => {
- return cm.client.get({
- contentTypeId: DOC_TYPE,
- id,
+ type LensGetResponseBody,
+ type LensCreateRequestBody,
+ type LensCreateResponseBody,
+ type LensUpdateRequestBody,
+ type LensUpdateResponseBody,
+ type LensSearchRequestQuery,
+ type LensSearchResponseBody,
+} from '../../server';
+
+export class LensClient {
+ constructor(private http: HttpStart) {}
+
+ // TODO add error handling logic with try/catch
+
+ async get(id: string) {
+ const { data, meta } = await this.http.get(`${LENS_VIS_API_PATH}/${id}`, {
+ version: LENS_API_VERSION,
});
- };
- const create = async ({ data, options }: Omit) => {
- const res = await cm.client.create({
- contentTypeId: DOC_TYPE,
- data,
+ return {
+ item: ConfigBuilderStub.in(data),
+ meta, // TODO: see if we still need this meta data
+ };
+ }
+
+ async create(
+ { description, visualizationType, state, title, version }: LensAttributes,
+ references: Reference[],
+ options: LensCreateRequestBody['options'] = {}
+ ) {
+ const body: LensCreateRequestBody = {
+ // TODO: Find a better way to conditionally omit id
+ data: omit(
+ ConfigBuilderStub.out({
+ id: '',
+ description,
+ visualizationType,
+ state,
+ title,
+ version,
+ references,
+ }),
+ 'id'
+ ),
options,
+ };
+
+ const { data, meta } = await this.http.post(LENS_VIS_API_PATH, {
+ body: JSON.stringify(body),
+ version: LENS_API_VERSION,
});
- return res;
- };
-
- const update = async ({ id, data, options }: Omit) => {
- const res = await cm.client.update({
- contentTypeId: DOC_TYPE,
- id,
- data,
+
+ return {
+ item: ConfigBuilderStub.in(data),
+ meta,
+ };
+ }
+
+ async update(
+ id: string,
+ { description, visualizationType, state, title, version }: LensAttributes,
+ references: Reference[],
+ options: LensUpdateRequestBody['options'] = {}
+ ) {
+ const body: LensUpdateRequestBody = {
+ // TODO: Find a better way to conditionally omit id
+ data: omit(
+ ConfigBuilderStub.out({
+ id: '',
+ description,
+ visualizationType,
+ state,
+ title,
+ version,
+ references,
+ }),
+ 'id'
+ ),
options,
- });
- return res;
- };
+ };
+
+ const { data, meta } = await this.http.put(
+ `${LENS_VIS_API_PATH}/${id}`,
+ {
+ body: JSON.stringify(body),
+ version: LENS_API_VERSION,
+ }
+ );
+
+ return {
+ item: ConfigBuilderStub.in(data),
+ meta,
+ };
+ }
- const deleteLens = async (id: string) => {
- const res = await cm.client.delete({
- contentTypeId: DOC_TYPE,
- id,
+ async delete(id: string): Promise<{ success: boolean }> {
+ const response = await this.http.delete(`${LENS_VIS_API_PATH}/${id}`, {
+ asResponse: true,
+ version: LENS_API_VERSION,
});
- return res;
- };
+ const success = response.response?.ok ?? false;
- const search = async (query: SearchQuery = {}, options?: LensSearchQuery) => {
- return cm.client.search({
- contentTypeId: DOC_TYPE,
- query,
- options,
+ return { success }; // TODO remove if not used
+ }
+
+ async search({
+ query,
+ page,
+ perPage,
+ fields,
+ searchFields,
+ }: LensSearchRequestQuery): Promise {
+ // TODO add all CM search options to query
+ const result = await this.http.get(LENS_VIS_API_PATH, {
+ query: {
+ query,
+ page,
+ perPage,
+ fields,
+ searchFields,
+ } satisfies LensSearchRequestQuery,
+ version: LENS_API_VERSION,
});
- };
-
- return {
- get,
- create,
- update,
- delete: deleteLens,
- search,
- } as unknown as VisualizationClient<'lens', Attr>;
+
+ return result.data.map(({ data }) => ({
+ ...data,
+ attributes: ConfigBuilderStub.in(data),
+ }));
+ }
}
diff --git a/x-pack/platform/plugins/shared/lens/public/persistence/lens_document_service.test.ts b/x-pack/platform/plugins/shared/lens/public/persistence/lens_document_service.test.ts
index 5d7353fab9c18..a6ec89a1c5d1c 100644
--- a/x-pack/platform/plugins/shared/lens/public/persistence/lens_document_service.test.ts
+++ b/x-pack/platform/plugins/shared/lens/public/persistence/lens_document_service.test.ts
@@ -5,30 +5,40 @@
* 2.0.
*/
-import { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
+import { coreMock } from '@kbn/core/public/mocks';
+
+import { LENS_ITEM_LATEST_VERSION } from '../../common/constants';
+import { LensClient } from './lens_client';
import { LensDocumentService } from './lens_document_service';
+import { LensDocument } from './types';
-describe('LensStore', () => {
- function testStore(testId?: string) {
- const client = {
- create: jest.fn(() => Promise.resolve({ item: { id: testId || 'testid' } })),
- update: jest.fn(() => Promise.resolve({ item: { id: testId || 'testid' } })),
- get: jest.fn(),
- };
+jest.mock('./lens_client', () => {
+ const mockClient = {
+ create: jest.fn(),
+ get: jest.fn(),
+ update: jest.fn(),
+ search: jest.fn(),
+ };
+ return {
+ LensClient: jest.fn(() => mockClient),
+ };
+});
+
+const startMock = coreMock.createStart();
+describe('LensStore', () => {
+ function testStore() {
+ const httpMock = startMock.http;
return {
- client,
- service: new LensDocumentService({
- client,
- registry: jest.fn(),
- } as unknown as ContentManagementPublicStart),
+ client: new LensClient(httpMock), // mock client
+ service: new LensDocumentService(httpMock),
};
}
describe('save', () => {
- test('creates and returns a visualization document', async () => {
- const { client, service } = testStore('FOO');
- const doc = await service.save({
+ test('creates and returns a Lens document', async () => {
+ const { client, service } = testStore();
+ const docToSave: LensDocument = {
title: 'Hello',
description: 'My doc',
visualizationType: 'bar',
@@ -41,50 +51,30 @@ describe('LensStore', () => {
query: { query: '', language: 'lucene' },
filters: [],
},
- });
+ version: LENS_ITEM_LATEST_VERSION,
+ };
+
+ jest.mocked(client.create).mockImplementation(async (item, references) => ({
+ item: { id: 'new-id', ...item, references, extraProp: 'test' },
+ meta: { type: 'lens' },
+ }));
+ const doc = await service.save(docToSave);
expect(doc).toEqual({
- savedObjectId: 'FOO',
- title: 'Hello',
- description: 'My doc',
- visualizationType: 'bar',
- references: [],
- state: {
- datasourceStates: {
- indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
- },
- visualization: { x: 'foo', y: 'baz' },
- query: { query: '', language: 'lucene' },
- filters: [],
- },
+ savedObjectId: 'new-id',
+ ...docToSave,
+ extraProp: 'test', // should replace doc with response properties
});
expect(client.create).toHaveBeenCalledTimes(1);
- expect(client.create).toHaveBeenCalledWith({
- contentTypeId: 'lens',
- data: {
- title: 'Hello',
- description: 'My doc',
- visualizationType: 'bar',
- state: {
- datasourceStates: {
- indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
- },
- visualization: { x: 'foo', y: 'baz' },
- query: { query: '', language: 'lucene' },
- filters: [],
- },
- },
- options: {
- references: [],
- },
- });
+ const { references, ...attributes } = docToSave;
+ expect(client.create).toHaveBeenCalledWith(attributes, references);
});
- test('updates and returns a visualization document', async () => {
- const { client, service } = testStore('Gandalf');
- const doc = await service.save({
- savedObjectId: 'Gandalf',
+ test('updates and returns a Lens document', async () => {
+ const { client, service } = testStore();
+ const docToUpdate: LensDocument = {
+ savedObjectId: 'update-id',
title: 'Even the very wise cannot see all ends.',
visualizationType: 'line',
references: [],
@@ -94,62 +84,30 @@ describe('LensStore', () => {
query: { query: '', language: 'lucene' },
filters: [],
},
- });
+ version: LENS_ITEM_LATEST_VERSION,
+ };
- expect(doc).toEqual({
- savedObjectId: 'Gandalf',
- title: 'Even the very wise cannot see all ends.',
- visualizationType: 'line',
- references: [],
- state: {
- datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
- visualization: { gear: ['staff', 'pointy hat'] },
- query: { query: '', language: 'lucene' },
- filters: [],
- },
- });
+ jest.mocked(client.update).mockImplementation(async (id, item, references) => ({
+ item: { id, ...item, references, extraProp: 'test' },
+ meta: { type: 'lens' },
+ }));
+
+ const doc = await service.save(docToUpdate);
+ // should replace doc with response properties
+ expect(doc).toEqual({ ...docToUpdate, extraProp: 'test' });
expect(client.update).toHaveBeenCalledTimes(1);
- expect(client.update.mock.calls).toEqual([
- [
- {
- contentTypeId: 'lens',
- id: 'Gandalf',
- data: {
- title: 'Even the very wise cannot see all ends.',
- visualizationType: 'line',
- state: {
- datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
- visualization: { gear: ['staff', 'pointy hat'] },
- query: { query: '', language: 'lucene' },
- filters: [],
- },
- },
- options: { references: [] },
- },
- ],
- ]);
+ const { savedObjectId, references, ...attributes } = docToUpdate;
+ expect(client.update).toHaveBeenCalledWith(savedObjectId, attributes, references);
});
});
describe('load', () => {
test('throws if an error is returned', async () => {
const { client, service } = testStore();
- client.get = jest.fn(async () => ({
- meta: { outcome: 'exactMatch' },
- item: {
- id: 'Paul',
- type: 'lens',
- attributes: {
- title: 'Hope clouds observation.',
- visualizationType: 'dune',
- state: '{ "datasource": { "giantWorms": true } }',
- },
- error: new Error('shoot dang!'),
- },
- }));
+ jest.mocked(client.get).mockRejectedValue(new Error('shoot dang!'));
- await expect(service.load('Paul')).rejects.toThrow('shoot dang!');
+ await expect(service.load('123')).rejects.toThrow('shoot dang!');
});
});
});
diff --git a/x-pack/platform/plugins/shared/lens/public/persistence/lens_document_service.ts b/x-pack/platform/plugins/shared/lens/public/persistence/lens_document_service.ts
index e0b12e56b159d..2e98339606fd2 100644
--- a/x-pack/platform/plugins/shared/lens/public/persistence/lens_document_service.ts
+++ b/x-pack/platform/plugins/shared/lens/public/persistence/lens_document_service.ts
@@ -5,14 +5,12 @@
* 2.0.
*/
-import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
-import type { SearchQuery } from '@kbn/content-management-plugin/common';
-import type { VisualizationClient } from '@kbn/visualizations-plugin/public';
+import { HttpStart } from '@kbn/core/public';
-import type { LensSavedObjectAttributes, LensSearchQuery } from '../../common/content_management';
-import { getLensClient } from './lens_client';
+import { LensClient } from './lens_client';
import { SAVE_DUPLICATE_REJECTED } from './constants';
import { LensDocument } from './types';
+import type { LensSearchRequestQuery } from '../../server';
export interface CheckDuplicateTitleOptions {
id?: string;
@@ -33,46 +31,40 @@ interface ILensDocumentService {
}
export class LensDocumentService implements ILensDocumentService {
- private client: VisualizationClient<'lens', LensSavedObjectAttributes>;
+ private client: LensClient;
- constructor(cm: ContentManagementPublicStart) {
- this.client = getLensClient(cm);
+ constructor(http: HttpStart) {
+ this.client = new LensClient(http);
}
save = async (vis: LensDocument) => {
- const { savedObjectId, type, references, ...attributes } = vis;
+ // TODO: Flatten LenDocument types to align with new LensItem, for now just keep it.
+ const { savedObjectId, references, ...attributes } = vis;
if (savedObjectId) {
- const result = await this.client.update({
- id: savedObjectId,
- data: attributes,
- options: {
- references,
- },
- });
- return { ...vis, savedObjectId: result.item.id };
+ const {
+ item: { id, ...newVis },
+ } = await this.client.update(savedObjectId, attributes, references);
+ return { ...newVis, savedObjectId: id };
}
- const result = await this.client.create({
- data: attributes,
- options: {
- references,
- },
- });
- return { ...vis, savedObjectId: result.item.id };
+
+ const {
+ item: { id: newId, ...newVis },
+ } = await this.client.create(attributes, references);
+
+ return { ...newVis, savedObjectId: newId };
};
async load(savedObjectId: string) {
- const resolveResult = await this.client.get(savedObjectId);
-
- if (resolveResult.item.error) {
- throw resolveResult.item.error;
+ try {
+ return await this.client.get(savedObjectId);
+ } catch (error) {
+ throw error;
}
-
- return resolveResult;
}
- async search(query: SearchQuery, options: LensSearchQuery) {
- const result = await this.client.search(query, options);
+ async search(options: LensSearchRequestQuery) {
+ const result = await this.client.search(options);
return result;
}
@@ -104,18 +96,13 @@ export class LensDocumentService implements ILensDocumentService {
// Elasticsearch will return the most relevant results first, which means exact matches should come
// first, and so we shouldn't need to request everything. Using 10 just to be on the safe side.
- const response = await this.search(
- {
- limit: 10,
- text: `"${title}"`,
- },
- {
- searchFields: ['title'],
- }
- );
- const duplicate = response.hits.find(
- (obj) => obj.attributes.title.toLowerCase() === title.toLowerCase()
- );
+ const response = await this.search({
+ perPage: 10,
+ query: `"${title}"`,
+ searchFields: ['title'],
+ });
+
+ const duplicate = response.find((item) => item.title.toLowerCase() === title.toLowerCase());
if (!duplicate || duplicate.id === id) {
return true;
diff --git a/x-pack/platform/plugins/shared/lens/public/persistence/types.ts b/x-pack/platform/plugins/shared/lens/public/persistence/types.ts
index 20b9073e4d230..5b3a0ef4f8105 100644
--- a/x-pack/platform/plugins/shared/lens/public/persistence/types.ts
+++ b/x-pack/platform/plugins/shared/lens/public/persistence/types.ts
@@ -8,13 +8,14 @@
import type { AggregateQuery, Filter, Query } from '@kbn/es-query';
import type { Reference } from '@kbn/content-management-utils';
import type { DataViewSpec } from '@kbn/data-views-plugin/public';
+import type { LENS_ITEM_LATEST_VERSION } from '../../common/constants';
export interface LensDocument {
savedObjectId?: string;
type?: string;
- visualizationType: string | null;
title: string;
description?: string;
+ visualizationType: string | null;
state: {
datasourceStates: Record;
visualization: unknown;
@@ -29,4 +30,5 @@ export interface LensDocument {
internalReferences?: Reference[];
};
references: Reference[];
+ version?: LENS_ITEM_LATEST_VERSION;
}
diff --git a/x-pack/platform/plugins/shared/lens/public/persistence/utils.ts b/x-pack/platform/plugins/shared/lens/public/persistence/utils.ts
new file mode 100644
index 0000000000000..baf82d5253cfc
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/public/persistence/utils.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.
+ */
+
+import type { LensItem, LensItemMeta, LensSavedObject } from '../../server/content_management';
+
+/**
+ * Converts Lens Response Item to Lens Saved Object
+ *
+ * This is only needed as the visualize plugin assumes we only use CM.
+ */
+export function getLensSOFromResponse({
+ item: { id, references, ...attributes },
+ meta: { type, createdAt, updatedAt, createdBy, updatedBy, managed, originId },
+}: {
+ item: LensItem;
+ meta: LensItemMeta;
+}): LensSavedObject {
+ return {
+ id,
+ references,
+ attributes,
+ type,
+ createdAt,
+ updatedAt,
+ createdBy,
+ updatedBy,
+ managed,
+ originId,
+ };
+}
diff --git a/x-pack/platform/plugins/shared/lens/public/plugin.ts b/x-pack/platform/plugins/shared/lens/public/plugin.ts
index 1168f938d4b6a..79647fc678f12 100644
--- a/x-pack/platform/plugins/shared/lens/public/plugin.ts
+++ b/x-pack/platform/plugins/shared/lens/public/plugin.ts
@@ -135,11 +135,8 @@ import { ChartInfoApi } from './chart_info_api';
import { type LensAppLocator, LensAppLocatorDefinition } from '../common/locator/locator';
import { downloadCsvLensShareProvider } from './app_plugin/csv_download_provider/csv_download_provider';
import type { LensDocument } from './persistence';
-import {
- CONTENT_ID,
- LATEST_VERSION,
- LensSavedObjectAttributes,
-} from '../common/content_management';
+import { LENS_CONTENT_TYPE, LENS_ITEM_LATEST_VERSION } from '../common/constants';
+import type { LensAttributes } from '../server/content_management';
import type { EditLensConfigurationProps } from './app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration';
import { LensRenderer } from './react_embeddable/renderer/lens_custom_renderer_component';
import {
@@ -363,7 +360,7 @@ export class LensPlugin {
return {
...plugins,
- attributeService: getLensAttributeService(plugins),
+ attributeService: getLensAttributeService(coreStart.http),
capabilities: coreStart.application.capabilities,
coreHttp: coreStart.http,
coreStart,
@@ -397,8 +394,13 @@ export class LensPlugin {
return createLensEmbeddableFactory(deps);
});
+ embeddable.registerTransforms(LENS_EMBEDDABLE_TYPE, async () => {
+ const { lensTransforms } = await import('../common/transforms/transforms');
+ return lensTransforms;
+ });
+
// Let Dashboard know about the Lens panel type
- embeddable.registerAddFromLibraryType({
+ embeddable.registerAddFromLibraryType({
onAdd: async (container, savedObject) => {
const { SAVED_OBJECT_REF_NAME } = await import('@kbn/presentation-publishing');
container.addNewPanel(
@@ -460,9 +462,9 @@ export class LensPlugin {
);
contentManagement.registry.register({
- id: CONTENT_ID,
+ id: LENS_CONTENT_TYPE,
version: {
- latest: LATEST_VERSION,
+ latest: LENS_ITEM_LATEST_VERSION,
},
name: i18n.translate('xpack.lens.content.name', {
defaultMessage: 'Lens Visualization',
@@ -516,7 +518,7 @@ export class LensPlugin {
const frameStart = this.editorFrameService!.start(coreStart, deps);
return mountApp(core, params, {
createEditorFrame: frameStart.createInstance,
- attributeService: getLensAttributeService(deps),
+ attributeService: getLensAttributeService(coreStart.http),
topNavMenuEntryGenerators: this.topNavMenuEntries,
locator: this.locator,
});
diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.ts
index 1015c58a43c6f..c75ff358fbf5e 100644
--- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.ts
+++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/esql.ts
@@ -13,7 +13,12 @@ import {
} from '@kbn/esql-utils';
import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils';
import { isESQLModeEnabled } from './initializers/utils';
-import type { LensEmbeddableStartServices } from './types';
+import type { LensEmbeddableStartServices, LensSerializedState } from './types';
+
+export type ESQLStartServices = Pick<
+ LensEmbeddableStartServices,
+ 'dataViews' | 'data' | 'visualizationMap' | 'datasourceMap' | 'uiSettings'
+>;
export async function loadESQLAttributes({
dataViews,
@@ -21,10 +26,7 @@ export async function loadESQLAttributes({
visualizationMap,
datasourceMap,
uiSettings,
-}: Pick<
- LensEmbeddableStartServices,
- 'dataViews' | 'data' | 'visualizationMap' | 'datasourceMap' | 'uiSettings'
->) {
+}: ESQLStartServices): Promise {
// Early exit if ESQL is not supported
if (!isESQLModeEnabled({ uiSettings })) {
return;
diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts
index 54e781f681e16..784a44843f2bf 100644
--- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts
+++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/helper.ts
@@ -25,11 +25,12 @@ import type {
LensSerializedState,
StructuredDatasourceStates,
} from './types';
-import { loadESQLAttributes } from './esql';
+import { ESQLStartServices, loadESQLAttributes } from './esql';
import { DatasourceStates, GeneralDatasourceStates } from '../state_management';
import { FormBasedPersistedState } from '../datasources/form_based/types';
import { TextBasedPersistedState } from '../datasources/form_based/esql_layer/types';
import { DOC_TYPE } from '../../common/constants';
+import { LENS_ITEM_LATEST_VERSION } from '../../common/constants';
export function createEmptyLensState(
visualizationType: null | string = null,
@@ -41,6 +42,7 @@ export function createEmptyLensState(
const isTextBased = query && isOfAggregateQueryType(query);
return {
attributes: {
+ version: LENS_ITEM_LATEST_VERSION,
title: title ?? '',
description: description ?? '',
visualizationType,
@@ -56,29 +58,23 @@ export function createEmptyLensState(
};
}
-// Shared logic to ensure the attributes are correctly loaded
-// Make sure to inject references from the container down to the runtime state
-// this ensure migrations/copy to spaces works correctly
+/**
+ * Shared logic to ensure the attributes are correctly loaded
+ * Make sure to inject references from the container down to the runtime state
+ * this ensure migrations/copy to spaces works correctly
+ **/
export async function deserializeState(
{
attributeService,
...services
- }: Pick<
- LensEmbeddableStartServices,
- | 'attributeService'
- | 'data'
- | 'dataViews'
- | 'data'
- | 'visualizationMap'
- | 'datasourceMap'
- | 'uiSettings'
- >,
+ }: Pick & ESQLStartServices,
rawState: LensSerializedState,
references?: Reference[]
-) {
+): Promise {
const fallbackAttributes = createEmptyLensState().attributes;
const savedObjectRef = findSavedObjectRef(DOC_TYPE, references);
const savedObjectId = savedObjectRef?.id ?? rawState.savedObjectId;
+
if (savedObjectId) {
try {
const { attributes, managed, sharingSavedObjectProps } =
@@ -89,11 +85,13 @@ export async function deserializeState(
return { ...rawState, attributes: fallbackAttributes };
}
}
+
// Inject applied only to by-value SOs
const newState = attributeService.injectReferences(
('attributes' in rawState ? rawState : { attributes: rawState }) as LensRuntimeState,
references?.length ? references : undefined
);
+
if (newState.isNewPanel) {
try {
const newAttributes = await loadESQLAttributes(services);
@@ -107,6 +105,7 @@ export async function deserializeState(
return { ...newState, attributes: fallbackAttributes };
}
}
+
return newState;
}
diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/inline_editing/state_management.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/inline_editing/state_management.tsx
index 2a4f1f48fd0dc..74334cd66b1d6 100644
--- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/inline_editing/state_management.tsx
+++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/inline_editing/state_management.tsx
@@ -47,9 +47,10 @@ export function getStateManagementForInlineEditing(
viz.state.adHocDataViews || {},
{ visualizationMap, datasourceMap, extractFilterReferences }
);
- const newDoc = {
+ const newDoc: TypedLensSerializedState['attributes'] = {
...viz,
...newViz,
+ visualizationType: newViz?.visualizationType ?? viz.visualizationType,
};
if (newDoc.state) {
diff --git a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx
index 885fda4c4aea2..a738d78ded647 100644
--- a/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx
+++ b/x-pack/platform/plugins/shared/lens/public/react_embeddable/renderer/lens_custom_renderer_component.tsx
@@ -140,6 +140,7 @@ export function LensRenderer({
type={LENS_EMBEDDABLE_TYPE}
maybeId={id}
+ // TODO type this ParentApi, all these are untyped and some unused
getParentApi={() => ({
// forward the Lens components to the embeddable
...props,
diff --git a/x-pack/platform/plugins/shared/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/platform/plugins/shared/lens/public/state_management/init_middleware/load_initial.ts
index 868b94cd9de89..4547866d8d388 100644
--- a/x-pack/platform/plugins/shared/lens/public/state_management/init_middleware/load_initial.ts
+++ b/x-pack/platform/plugins/shared/lens/public/state_management/init_middleware/load_initial.ts
@@ -8,7 +8,14 @@
import { MiddlewareAPI } from '@reduxjs/toolkit';
import { i18n } from '@kbn/i18n';
import { History } from 'history';
-import { setState, initExisting, initEmpty, LensStoreDeps, LensAppState } from '..';
+import {
+ setState,
+ initExisting,
+ initEmpty,
+ LensStoreDeps,
+ LensAppState,
+ VisualizationState,
+} from '..';
import { type InitialAppState, disableAutoApply, getPreloadedState } from '../lens_slice';
import { SharingSavedObjectProps } from '../../types';
import { getInitialDatasourceId, getInitialDataViewsObject } from '../../utils';
@@ -252,7 +259,7 @@ async function loadFromSavedObject(
data.query.filterManager.setAppFilters(filters);
}
- const docVisualizationState = {
+ const docVisualizationState: VisualizationState = {
activeId: doc.visualizationType,
state: doc.state.visualization,
};
diff --git a/x-pack/platform/plugins/shared/lens/public/state_management/shared_logic.ts b/x-pack/platform/plugins/shared/lens/public/state_management/shared_logic.ts
index 2e9ac200879fa..999816a3dac09 100644
--- a/x-pack/platform/plugins/shared/lens/public/state_management/shared_logic.ts
+++ b/x-pack/platform/plugins/shared/lens/public/state_management/shared_logic.ts
@@ -14,6 +14,7 @@ import { DOC_TYPE, INDEX_PATTERN_TYPE } from '../../common/constants';
import { VisualizationState, DatasourceStates } from '.';
import { LensDocument } from '../persistence';
import { DatasourceMap, VisualizationMap, Datasource } from '../types';
+import { LENS_ITEM_LATEST_VERSION } from '../../common/constants';
// This piece of logic is shared between the main editor code base and the inline editor one within the embeddable
export function mergeToNewDoc(
@@ -33,7 +34,7 @@ export function mergeToNewDoc(
visualizationMap: VisualizationMap;
extractFilterReferences: FilterManager['extract'];
}
-) {
+): LensDocument | undefined {
const activeVisualization =
visualization.state && visualization.activeId ? visualizationMap[visualization.activeId] : null;
const activeDatasource =
@@ -124,7 +125,8 @@ export function mergeToNewDoc(
internalReferences,
adHocDataViews: persistableAdHocDataViews,
},
- };
+ version: LENS_ITEM_LATEST_VERSION,
+ } satisfies LensDocument;
}
export function getActiveDataFromDatatable(
diff --git a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx
index b00cf33bcd911..733bd685191c8 100644
--- a/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx
+++ b/x-pack/platform/plugins/shared/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action.test.tsx
@@ -11,6 +11,7 @@ import { createMockStartDependencies } from '../../../editor_frame_service/mocks
import { EditLensEmbeddableAction } from './in_app_embeddable_edit_action';
import { TypedLensSerializedState } from '../../../react_embeddable/types';
import { BehaviorSubject } from 'rxjs';
+import { LENS_ITEM_LATEST_VERSION } from '../../../../common/constants';
describe('inapp editing of Lens embeddable', () => {
const core = coreMock.createStart();
@@ -33,6 +34,7 @@ describe('inapp editing of Lens embeddable', () => {
visualization: {},
},
references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }],
+ version: LENS_ITEM_LATEST_VERSION,
} as TypedLensSerializedState['attributes'];
it('is incompatible for ESQL charts and if ui setting for ES|QL is off', async () => {
const inAppEditAction = new EditLensEmbeddableAction(core, {
diff --git a/x-pack/platform/plugins/shared/lens/public/vis_type_alias.ts b/x-pack/platform/plugins/shared/lens/public/vis_type_alias.ts
index cf6e11c84ac7a..8f986c3770118 100644
--- a/x-pack/platform/plugins/shared/lens/public/vis_type_alias.ts
+++ b/x-pack/platform/plugins/shared/lens/public/vis_type_alias.ts
@@ -15,7 +15,7 @@ import {
LENS_ICON,
STAGE_ID,
} from '../common/constants';
-import { getLensClient } from './persistence/lens_client';
+import { getLensBasicClient } from './persistence/basic_lens_client';
export const lensVisTypeAlias: VisTypeAlias = {
alias: {
@@ -39,7 +39,7 @@ export const lensVisTypeAlias: VisTypeAlias = {
docTypes: [LENS_EMBEDDABLE_TYPE],
searchFields: ['title^3'],
clientOptions: { update: { overwrite: true } },
- client: getLensClient,
+ client: getLensBasicClient,
toListItem(savedObject) {
const { id, type, updatedAt, attributes, managed } = savedObject;
const { title, description } = attributes;
diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/xy/persistence.ts b/x-pack/platform/plugins/shared/lens/public/visualizations/xy/persistence.ts
index 830cb38f22866..fda5ab3700548 100644
--- a/x-pack/platform/plugins/shared/lens/public/visualizations/xy/persistence.ts
+++ b/x-pack/platform/plugins/shared/lens/public/visualizations/xy/persistence.ts
@@ -23,16 +23,11 @@ import { isAnnotationsLayer, isByReferenceAnnotationsLayer } from './visualizati
import { nonNullable } from '../../utils';
import { annotationLayerHasUnsavedChanges } from './state_helpers';
-export const isPersistedByReferenceAnnotationsLayer = (
+const isPersistedByReferenceAnnotationsLayer = (
layer: XYPersistedAnnotationLayerConfig
): layer is XYPersistedByReferenceAnnotationLayerConfig =>
isPersistedAnnotationsLayer(layer) && layer.persistanceType === 'byReference';
-export const isPersistedLinkedByValueAnnotationsLayer = (
- layer: XYPersistedAnnotationLayerConfig
-): layer is XYPersistedLinkedByValueAnnotationLayerConfig =>
- isPersistedAnnotationsLayer(layer) && layer.persistanceType === 'linked';
-
/**
* This is the type of hybrid layer we get after the user has made a change to
* a by-reference annotation layer and saved the visualization without
@@ -111,9 +106,10 @@ export function convertToPersistable(state: XYState) {
persistableLayers.push({ ...persistableLayer, persistanceType: 'byValue' });
return;
}
- /**
+
+ /*
* by reference annotation layer needs to be handled carefully
- **/
+ */
// make this id stable so that it won't retrigger all the time a change diff
const referenceName = `ref-${layer.layerId}`;
@@ -158,7 +154,11 @@ export function convertToPersistable(state: XYState) {
name: getLayerReferenceName(layer.layerId),
});
});
- return { references, state: { ...persistableState, layers: persistableLayers } };
+
+ return {
+ references,
+ state: { ...persistableState, layers: persistableLayers },
+ };
}
export const isPersistedAnnotationsLayer = (
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/index.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/index.ts
index 9515f4da9df0b..8fca254df9094 100644
--- a/x-pack/platform/plugins/shared/lens/server/api/routes/index.ts
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/index.ts
@@ -11,3 +11,5 @@ import { registerLensVisualizationsAPIRoutes } from './visualizations';
export function registerLensAPIRoutes(args: RegisterAPIRoutesArgs) {
registerLensVisualizationsAPIRoutes(args);
}
+
+export * from './schema';
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/schema.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/schema.ts
new file mode 100644
index 0000000000000..7091bb3f6b898
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/schema.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 * from './visualizations/schema';
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/types.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/types.ts
new file mode 100644
index 0000000000000..8b6ccbffe8615
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/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 * from './visualizations/types';
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/utils.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/utils.ts
new file mode 100644
index 0000000000000..ba060788f4945
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/utils.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { LensResponseItem, LensSavedObject } from '../../content_management';
+import { ConfigBuilderStub } from '../../../common/transforms';
+
+/**
+ * Converts Lens Saved Object to Lens Response Item
+ */
+export function getLensResponseItem({
+ // Data params
+ id,
+ references,
+ attributes,
+
+ // Meta params
+ type,
+ createdAt,
+ updatedAt,
+ createdBy,
+ updatedBy,
+ managed,
+ originId,
+}: LensSavedObject): LensResponseItem {
+ return {
+ data: ConfigBuilderStub.out({
+ ...attributes,
+ id,
+ references,
+ }),
+ meta: {
+ type,
+ createdAt,
+ updatedAt,
+ createdBy,
+ updatedBy,
+ managed,
+ originId,
+ },
+ };
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/create.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/create.ts
index e1fc00bf06bab..232626b944f02 100644
--- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/create.ts
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/create.ts
@@ -5,30 +5,31 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
-
+import { omit } from 'lodash';
import { boomify, isBoom } from '@hapi/boom';
-import { CONTENT_ID, type LensSavedObject } from '../../../../common/content_management';
-import {
- PUBLIC_API_PATH,
- PUBLIC_API_VERSION,
- PUBLIC_API_CONTENT_MANAGEMENT_VERSION,
- PUBLIC_API_ACCESS,
-} from '../../constants';
+
+import { TypeOf } from '@kbn/config-schema';
+
import {
- lensAttributesSchema,
- lensCreateOptionsSchema,
- lensSavedObjectSchema,
-} from '../../../content_management/v1';
+ LENS_VIS_API_PATH,
+ LENS_API_VERSION,
+ LENS_API_ACCESS,
+ LENS_CONTENT_TYPE,
+} from '../../../../common/constants';
+import type { LensCreateIn, LensSavedObject } from '../../../content_management';
import { RegisterAPIRouteFn } from '../../types';
+import { ConfigBuilderStub } from '../../../../common/transforms';
+import { lensCreateRequestBodySchema, lensCreateResponseBodySchema } from './schema';
+import { getLensResponseItem } from '../utils';
+import { isNewApiFormat } from '../../../../common/transforms/config_builder_stub';
export const registerLensVisualizationsCreateAPIRoute: RegisterAPIRouteFn = (
router,
{ contentManagement }
) => {
const createRoute = router.post({
- path: `${PUBLIC_API_PATH}/visualizations`,
- access: PUBLIC_API_ACCESS,
+ path: LENS_VIS_API_PATH,
+ access: LENS_API_ACCESS,
enableQueryVersion: true,
summary: 'Create Lens visualization',
description: 'Create a new Lens visualization.',
@@ -48,17 +49,14 @@ export const registerLensVisualizationsCreateAPIRoute: RegisterAPIRouteFn = (
createRoute.addVersion(
{
- version: PUBLIC_API_VERSION,
+ version: LENS_API_VERSION,
validate: {
request: {
- body: schema.object({
- options: lensCreateOptionsSchema,
- data: lensAttributesSchema,
- }),
+ body: lensCreateRequestBodySchema,
},
response: {
201: {
- body: () => lensSavedObjectSchema,
+ body: () => lensCreateResponseBodySchema,
description: 'Created',
},
400: {
@@ -77,14 +75,34 @@ export const registerLensVisualizationsCreateAPIRoute: RegisterAPIRouteFn = (
},
},
async (ctx, req, res) => {
- let result;
- const { data, options } = req.body;
+ // TODO fix IContentClient to type this client based on the actual
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
- .for(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
+ .for(LENS_CONTENT_TYPE);
+
+ const { references, ...lensItem } = isNewApiFormat(req.body.data)
+ ? // TODO: Find a better way to conditionally omit id
+ omit(ConfigBuilderStub.in(req.body.data), 'id')
+ : // For now we need to be able to create old SO, this may be moved to the config builder
+ ({
+ ...req.body.data,
+ description: req.body.data.description ?? undefined,
+ visualizationType: req.body.data.visualizationType ?? null,
+ } satisfies LensCreateIn['data']);
try {
- ({ result } = await client.create(data, options));
+ // Note: these types are to enforce loose param typings of client methods
+ const data: LensCreateIn['data'] = lensItem;
+ const options: LensCreateIn['options'] = { ...req.body.options, references };
+ const { result } = await client.create(data, options);
+
+ if (result.item.error) {
+ throw result.item.error;
+ }
+
+ return res.created>({
+ body: getLensResponseItem(result.item),
+ });
} catch (error) {
if (isBoom(error) && error.output.statusCode === 403) {
return res.forbidden();
@@ -92,8 +110,6 @@ export const registerLensVisualizationsCreateAPIRoute: RegisterAPIRouteFn = (
return boomify(error); // forward unknown error
}
-
- return res.created({ body: result.item });
}
);
};
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/delete.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/delete.ts
index b8a596fac0255..a1ea770fa0cbc 100644
--- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/delete.ts
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/delete.ts
@@ -5,25 +5,24 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
-
import { boomify, isBoom } from '@hapi/boom';
-import { CONTENT_ID, type LensSavedObject } from '../../../../common/content_management';
import {
- PUBLIC_API_PATH,
- PUBLIC_API_VERSION,
- PUBLIC_API_CONTENT_MANAGEMENT_VERSION,
- PUBLIC_API_ACCESS,
-} from '../../constants';
+ LENS_VIS_API_PATH,
+ LENS_API_VERSION,
+ LENS_API_ACCESS,
+ LENS_CONTENT_TYPE,
+} from '../../../../common/constants';
+import type { LensSavedObject } from '../../../content_management';
import { RegisterAPIRouteFn } from '../../types';
+import { lensDeleteRequestParamsSchema } from './schema';
export const registerLensVisualizationsDeleteAPIRoute: RegisterAPIRouteFn = (
router,
{ contentManagement }
) => {
const deleteRoute = router.delete({
- path: `${PUBLIC_API_PATH}/visualizations/{id}`,
- access: PUBLIC_API_ACCESS,
+ path: `${LENS_VIS_API_PATH}/{id}`,
+ access: LENS_API_ACCESS,
enableQueryVersion: true,
summary: 'Delete Lens visualization',
description: 'Delete a Lens visualization by id.',
@@ -43,16 +42,10 @@ export const registerLensVisualizationsDeleteAPIRoute: RegisterAPIRouteFn = (
deleteRoute.addVersion(
{
- version: PUBLIC_API_VERSION,
+ version: LENS_API_VERSION,
validate: {
request: {
- params: schema.object({
- id: schema.string({
- meta: {
- description: 'The saved object id of a Lens visualization.',
- },
- }),
- }),
+ params: lensDeleteRequestParamsSchema,
},
response: {
204: {
@@ -77,18 +70,25 @@ export const registerLensVisualizationsDeleteAPIRoute: RegisterAPIRouteFn = (
},
},
async (ctx, req, res) => {
+ // TODO fix IContentClient to type this client based on the actual
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
- .for(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
+ .for(LENS_CONTENT_TYPE);
try {
- await client.delete(req.params.id);
+ const { result } = await client.delete(req.params.id);
+
+ if (!result.success) {
+ throw new Error(`Failed to delete Lens visualization with id [${req.params.id}].`);
+ }
+
+ return res.noContent();
} catch (error) {
if (isBoom(error)) {
if (error.output.statusCode === 404) {
return res.notFound({
body: {
- message: `A Lens visualization with saved object id [${req.params.id}] was not found.`,
+ message: `A Lens visualization with id [${req.params.id}] was not found.`,
},
});
}
@@ -99,8 +99,6 @@ export const registerLensVisualizationsDeleteAPIRoute: RegisterAPIRouteFn = (
return boomify(error); // forward unknown error
}
-
- return res.noContent();
}
);
};
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/get.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/get.ts
index 185a897bf398d..ca7a79cb58aaf 100644
--- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/get.ts
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/get.ts
@@ -5,26 +5,28 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
-
import { boomify, isBoom } from '@hapi/boom';
-import { CONTENT_ID, type LensSavedObject } from '../../../../common/content_management';
+
+import { TypeOf } from '@kbn/config-schema';
+
import {
- PUBLIC_API_PATH,
- PUBLIC_API_VERSION,
- PUBLIC_API_CONTENT_MANAGEMENT_VERSION,
- PUBLIC_API_ACCESS,
-} from '../../constants';
-import { lensSavedObjectSchema } from '../../../content_management/v1';
+ LENS_VIS_API_PATH,
+ LENS_API_VERSION,
+ LENS_API_ACCESS,
+ LENS_CONTENT_TYPE,
+} from '../../../../common/constants';
+import type { LensSavedObject } from '../../../content_management';
import { RegisterAPIRouteFn } from '../../types';
+import { lensGetRequestParamsSchema, lensGetResponseBodySchema } from './schema';
+import { getLensResponseItem } from '../utils';
export const registerLensVisualizationsGetAPIRoute: RegisterAPIRouteFn = (
router,
{ contentManagement }
) => {
const getRoute = router.get({
- path: `${PUBLIC_API_PATH}/visualizations/{id}`,
- access: PUBLIC_API_ACCESS,
+ path: `${LENS_VIS_API_PATH}/{id}`,
+ access: LENS_API_ACCESS,
enableQueryVersion: true,
summary: 'Get Lens visualization',
description: 'Get a Lens visualization from id.',
@@ -44,20 +46,14 @@ export const registerLensVisualizationsGetAPIRoute: RegisterAPIRouteFn = (
getRoute.addVersion(
{
- version: PUBLIC_API_VERSION,
+ version: LENS_API_VERSION,
validate: {
request: {
- params: schema.object({
- id: schema.string({
- meta: {
- description: 'The saved object id of a Lens visualization.',
- },
- }),
- }),
+ params: lensGetRequestParamsSchema,
},
response: {
200: {
- body: () => lensSavedObjectSchema,
+ body: () => lensGetResponseBodySchema,
description: 'Ok',
},
400: {
@@ -79,19 +75,34 @@ export const registerLensVisualizationsGetAPIRoute: RegisterAPIRouteFn = (
},
},
async (ctx, req, res) => {
- let result;
+ // TODO fix IContentClient to type this client based on the actual
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
- .for(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
+ .for(LENS_CONTENT_TYPE);
try {
- ({ result } = await client.get(req.params.id));
+ const { result } = await client.get(req.params.id);
+
+ if (result.item.error) {
+ throw result.item.error;
+ }
+
+ const body = getLensResponseItem(result.item);
+ return res.ok>({
+ body: {
+ ...body,
+ meta: {
+ ...body.meta,
+ ...result.meta,
+ },
+ },
+ });
} catch (error) {
if (isBoom(error)) {
if (error.output.statusCode === 404) {
return res.notFound({
body: {
- message: `A Lens visualization with saved object id [${req.params.id}] was not found.`,
+ message: `A Lens visualization with id [${req.params.id}] was not found.`,
},
});
}
@@ -102,8 +113,6 @@ export const registerLensVisualizationsGetAPIRoute: RegisterAPIRouteFn = (
return boomify(error); // forward unknown error
}
-
- return res.ok({ body: result.item });
}
);
};
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/create.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/create.ts
new file mode 100644
index 0000000000000..7b57caf26b49d
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/create.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 {
+ pickFromObjectSchema,
+ lensResponseItemSchema,
+ lensAPIConfigSchema,
+ lensAPIAttributesSchema,
+ lensCMCreateOptionsSchema,
+} from '../../../../content_management';
+import { lensCreateRequestBodyDataSchemaV0 } from '../../../../content_management/v0';
+
+const apiConfigData = schema.object(
+ {
+ ...lensAPIAttributesSchema.getPropSchemas(),
+ // omit id on create options
+ ...pickFromObjectSchema(lensAPIConfigSchema.getPropSchemas(), ['references']),
+ },
+ { unknowns: 'forbid' }
+);
+
+export const lensCreateRequestBodySchema = schema.object(
+ {
+ // Permit passing old v0 SO attributes on create
+ data: schema.oneOf([apiConfigData, lensCreateRequestBodyDataSchemaV0]),
+ // TODO should these options be here or in params?
+ options: schema.object(
+ {
+ ...pickFromObjectSchema(lensCMCreateOptionsSchema.getPropSchemas(), ['overwrite']),
+ },
+ { unknowns: 'forbid' }
+ ),
+ },
+ { unknowns: 'forbid' }
+);
+
+export const lensCreateResponseBodySchema = lensResponseItemSchema;
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/delete.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/delete.ts
new file mode 100644
index 0000000000000..a8885fefa4880
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/delete.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 { schema } from '@kbn/config-schema';
+
+export const lensDeleteRequestParamsSchema = schema.object(
+ {
+ id: schema.string({
+ meta: {
+ description: 'The saved object id of a Lens visualization.',
+ },
+ }),
+ },
+ { unknowns: 'forbid' }
+);
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/get.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/get.ts
new file mode 100644
index 0000000000000..a65da3195fc47
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/get.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 { lensCMGetResultSchema, lensResponseItemSchema } from '../../../../content_management';
+
+export const lensGetRequestParamsSchema = schema.object(
+ {
+ id: schema.string({
+ meta: {
+ description: 'The saved object id of a Lens visualization.',
+ },
+ }),
+ },
+ { unknowns: 'forbid' }
+);
+
+export const lensGetResponseBodySchema = schema.object(
+ {
+ data: lensResponseItemSchema.getPropSchemas().data,
+ meta: schema.object(
+ {
+ ...lensCMGetResultSchema.getPropSchemas().meta.getPropSchemas(), // include CM meta data
+ ...lensResponseItemSchema.getPropSchemas().meta.getPropSchemas(),
+ },
+ { unknowns: 'forbid' }
+ ),
+ },
+ { unknowns: 'forbid' }
+);
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/index.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/index.ts
new file mode 100644
index 0000000000000..6098db1fceab8
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/index.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 * from './get';
+export * from './create';
+export * from './update';
+export * from './delete';
+export * from './search';
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/search.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/search.ts
new file mode 100644
index 0000000000000..02e21bd0d683a
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/search.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 { searchOptionsSchemas } from '@kbn/content-management-utils';
+
+import {
+ pickFromObjectSchema,
+ lensCMSearchOptionsSchema,
+ lensResponseItemSchema,
+} from '../../../../content_management';
+
+// TODO cleanup and align search options types with client side options
+// TODO align defaults with cm and other schema definitions (i.e. searchOptionsSchemas)
+// TODO See if these should be in body or params?
+export const lensSearchRequestQuerySchema = schema.object({
+ ...lensCMSearchOptionsSchema.getPropSchemas(),
+ query: schema.maybe(
+ schema.string({
+ meta: {
+ description: 'The text to search for Lens visualizations',
+ },
+ })
+ ),
+ page: schema.number({
+ meta: {
+ description: 'Specifies the current page number of the paginated result.',
+ },
+ min: 1,
+ defaultValue: 1,
+ }),
+ perPage: schema.number({
+ meta: {
+ description: 'Maximum number of Lens visualizations included in a single response',
+ },
+ defaultValue: 20,
+ min: 1,
+ max: 1000,
+ }),
+});
+
+const lensSearchResponseMetaSchema = schema.object(
+ {
+ ...pickFromObjectSchema(searchOptionsSchemas, ['page', 'perPage']),
+ total: schema.number(), // TODO use shared definition
+ },
+ { unknowns: 'forbid' }
+);
+
+export const lensSearchResponseBodySchema = schema.object(
+ {
+ data: schema.arrayOf(lensResponseItemSchema),
+ meta: lensSearchResponseMetaSchema,
+ },
+ { unknowns: 'forbid' }
+);
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/update.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/update.ts
new file mode 100644
index 0000000000000..e9792ab339001
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/schema/update.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { omit } from 'lodash';
+import {
+ lensResponseItemSchema,
+ lensAPIAttributesSchema,
+ pickFromObjectSchema,
+ lensAPIConfigSchema,
+ lensCMUpdateOptionsSchema,
+} from '../../../../content_management';
+
+export const lensUpdateRequestParamsSchema = schema.object(
+ {
+ id: schema.string({
+ meta: {
+ description: 'The saved object id of a Lens visualization.',
+ },
+ }),
+ },
+ { unknowns: 'forbid' }
+);
+
+export const lensUpdateRequestBodySchema = schema.object(
+ {
+ data: schema.object(
+ {
+ ...lensAPIAttributesSchema.getPropSchemas(),
+ // omit id on create options
+ ...pickFromObjectSchema(lensAPIConfigSchema.getPropSchemas(), ['references']),
+ },
+ { unknowns: 'forbid' }
+ ),
+ // TODO should these options be here?
+ options: schema.object(
+ {
+ ...omit(lensCMUpdateOptionsSchema.getPropSchemas(), ['references']),
+ },
+ { unknowns: 'forbid' }
+ ),
+ },
+ {
+ unknowns: 'forbid',
+ }
+);
+
+export const lensUpdateResponseBodySchema = lensResponseItemSchema;
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/search.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/search.ts
index 3ab47a50824c0..f87e1cc1d64c8 100644
--- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/search.ts
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/search.ts
@@ -5,26 +5,27 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
-
import { isBoom, boomify } from '@hapi/boom';
-import { CONTENT_ID, type LensSavedObject } from '../../../../common/content_management';
+
+import { TypeOf } from '@kbn/config-schema';
import {
- PUBLIC_API_PATH,
- PUBLIC_API_VERSION,
- PUBLIC_API_CONTENT_MANAGEMENT_VERSION,
- PUBLIC_API_ACCESS,
-} from '../../constants';
-import { lensSavedObjectSchema } from '../../../content_management/v1';
+ LENS_VIS_API_PATH,
+ LENS_API_VERSION,
+ LENS_API_ACCESS,
+ LENS_CONTENT_TYPE,
+} from '../../../../common/constants';
+import type { LensSearchIn, LensSavedObject } from '../../../content_management';
import { RegisterAPIRouteFn } from '../../types';
+import { lensSearchRequestQuerySchema, lensSearchResponseBodySchema } from './schema';
+import { getLensResponseItem } from '../utils';
export const registerLensVisualizationsSearchAPIRoute: RegisterAPIRouteFn = (
router,
{ contentManagement }
) => {
const searchRoute = router.get({
- path: `${PUBLIC_API_PATH}/visualizations`,
- access: PUBLIC_API_ACCESS,
+ path: LENS_VIS_API_PATH,
+ access: LENS_API_ACCESS,
enableQueryVersion: true,
summary: 'Search Lens visualizations',
description: 'Get list of Lens visualizations.',
@@ -44,37 +45,14 @@ export const registerLensVisualizationsSearchAPIRoute: RegisterAPIRouteFn = (
searchRoute.addVersion(
{
- version: PUBLIC_API_VERSION,
+ version: LENS_API_VERSION,
validate: {
request: {
- query: schema.object({
- query: schema.maybe(
- schema.string({
- meta: {
- description: 'The text to search for Lens visualizations',
- },
- })
- ),
- page: schema.number({
- meta: {
- description: 'Specifies the current page number of the paginated result.',
- },
- min: 1,
- defaultValue: 1,
- }),
- perPage: schema.number({
- meta: {
- description: 'Maximum number of Lens visualizations included in a single response',
- },
- defaultValue: 20,
- min: 1,
- max: 1000,
- }),
- }),
+ query: lensSearchRequestQuerySchema,
},
response: {
200: {
- body: () => schema.arrayOf(lensSavedObjectSchema),
+ body: () => lensSearchResponseBodySchema,
description: 'Ok',
},
400: {
@@ -93,23 +71,44 @@ export const registerLensVisualizationsSearchAPIRoute: RegisterAPIRouteFn = (
},
},
async (ctx, req, res) => {
- let result;
- const { query, page, perPage: limit } = req.query;
+ // TODO fix IContentClient to type this client based on the actual
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
- .for(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
+ .for(LENS_CONTENT_TYPE);
+
+ const { query: q, page, perPage, ...reqOptions } = req.query;
try {
- ({ result } = await client.search(
- {
- text: query,
- cursor: page.toString(),
- limit,
+ // Note: these types are to enforce loose param typings of client methods
+ const query: LensSearchIn['query'] = {
+ text: q,
+ cursor: page.toString(),
+ limit: perPage,
+ };
+ const options: LensSearchIn['options'] = reqOptions;
+
+ const {
+ result: { hits, pagination },
+ } = await client.search(query, options);
+
+ // TODO: see if this check is actually needed
+ const error = hits.find((item) => item.error);
+ if (error) {
+ throw error;
+ }
+
+ return res.ok>({
+ body: {
+ data: hits.map((item) => {
+ return getLensResponseItem(item);
+ }),
+ meta: {
+ page,
+ perPage,
+ total: pagination.total,
+ },
},
- {
- searchFields: ['title', 'description'],
- }
- ));
+ });
} catch (error) {
if (isBoom(error) && error.output.statusCode === 403) {
return res.forbidden();
@@ -117,8 +116,6 @@ export const registerLensVisualizationsSearchAPIRoute: RegisterAPIRouteFn = (
return boomify(error); // forward unknown error
}
-
- return res.ok({ body: result.hits });
}
);
};
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/types.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/types.ts
new file mode 100644
index 0000000000000..e9c435a9e65f9
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/types.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { TypeOf } from '@kbn/config-schema';
+
+import { Optional } from 'utility-types';
+import {
+ lensCreateRequestBodySchema,
+ lensCreateResponseBodySchema,
+ lensDeleteRequestParamsSchema,
+ lensGetRequestParamsSchema,
+ lensGetResponseBodySchema,
+ lensSearchRequestQuerySchema,
+ lensSearchResponseBodySchema,
+ lensUpdateRequestBodySchema,
+ lensUpdateRequestParamsSchema,
+ lensUpdateResponseBodySchema,
+} from './schema';
+
+export type LensCreateRequestBody = TypeOf;
+export type LensCreateResponseBody = TypeOf;
+
+export type LensUpdateRequestParams = TypeOf;
+export type LensUpdateRequestBody = TypeOf;
+export type LensUpdateResponseBody = TypeOf;
+
+export type LensGetRequestParams = TypeOf;
+export type LensGetResponseBody = TypeOf;
+
+export type LensSearchRequestQuery = Optional<
+ // TODO: find out why default values show as required, adding maybe returns undefined values
+ TypeOf,
+ 'page' | 'perPage'
+>;
+export type LensSearchResponseBody = TypeOf;
+
+export type LensDeleteRequestParams = TypeOf;
diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/update.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/update.ts
index 1f3279a5976dd..dddbd2434697e 100644
--- a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/update.ts
+++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/update.ts
@@ -5,30 +5,33 @@
* 2.0.
*/
-import { schema } from '@kbn/config-schema';
import { boomify, isBoom } from '@hapi/boom';
-import { CONTENT_ID, type LensSavedObject } from '../../../../common/content_management';
+import { TypeOf } from '@kbn/config-schema';
+import { omit } from 'lodash';
import {
- PUBLIC_API_PATH,
- PUBLIC_API_VERSION,
- PUBLIC_API_CONTENT_MANAGEMENT_VERSION,
- PUBLIC_API_ACCESS,
-} from '../../constants';
-import {
- lensAttributesSchema,
- lensCreateOptionsSchema,
- lensSavedObjectSchema,
-} from '../../../content_management/v1';
+ LENS_VIS_API_PATH,
+ LENS_API_VERSION,
+ LENS_API_ACCESS,
+ LENS_CONTENT_TYPE,
+} from '../../../../common/constants';
+import type { LensUpdateIn, LensSavedObject } from '../../../content_management';
import { RegisterAPIRouteFn } from '../../types';
+import { ConfigBuilderStub } from '../../../../common/transforms';
+import {
+ lensUpdateRequestBodySchema,
+ lensUpdateRequestParamsSchema,
+ lensUpdateResponseBodySchema,
+} from './schema';
+import { getLensResponseItem } from '../utils';
export const registerLensVisualizationsUpdateAPIRoute: RegisterAPIRouteFn = (
router,
{ contentManagement }
) => {
const updateRoute = router.put({
- path: `${PUBLIC_API_PATH}/visualizations/{id}`,
- access: PUBLIC_API_ACCESS,
+ path: `${LENS_VIS_API_PATH}/{id}`,
+ access: LENS_API_ACCESS,
enableQueryVersion: true,
summary: 'Update Lens visualization',
description: 'Update an existing Lens visualization.',
@@ -48,24 +51,15 @@ export const registerLensVisualizationsUpdateAPIRoute: RegisterAPIRouteFn = (
updateRoute.addVersion(
{
- version: PUBLIC_API_VERSION,
+ version: LENS_API_VERSION,
validate: {
request: {
- params: schema.object({
- id: schema.string({
- meta: {
- description: 'The saved object id of a Lens visualization.',
- },
- }),
- }),
- body: schema.object({
- options: lensCreateOptionsSchema,
- data: lensAttributesSchema,
- }),
+ params: lensUpdateRequestParamsSchema,
+ body: lensUpdateRequestBodySchema,
},
response: {
200: {
- body: () => lensSavedObjectSchema,
+ body: () => lensUpdateResponseBodySchema,
description: 'Ok',
},
400: {
@@ -87,20 +81,38 @@ export const registerLensVisualizationsUpdateAPIRoute: RegisterAPIRouteFn = (
},
},
async (ctx, req, res) => {
- let result;
- const { data, options } = req.body;
+ // TODO fix IContentClient to type this client based on the actual
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
- .for(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
+ .for(LENS_CONTENT_TYPE);
+
+ const { references, ...lensItem } = omit(
+ ConfigBuilderStub.in({
+ id: '', // TODO: Find a better way to conditionally omit id
+ ...req.body.data,
+ }),
+ 'id'
+ );
try {
- ({ result } = await client.update(req.params.id, data, options));
+ // Note: these types are to enforce loose param typings of client methods
+ const data: LensUpdateIn['data'] = lensItem;
+ const options: LensUpdateIn['options'] = { references };
+ const { result } = await client.update(req.params.id, data, options);
+
+ if (result.item.error) {
+ throw result.item.error;
+ }
+
+ return res.ok>({
+ body: getLensResponseItem(result.item),
+ });
} catch (error) {
if (isBoom(error)) {
if (error.output.statusCode === 404) {
return res.notFound({
body: {
- message: `A Lens visualization with saved object id [${req.params.id}] was not found.`,
+ message: `A Lens visualization with id [${req.params.id}] was not found.`,
},
});
}
@@ -111,8 +123,6 @@ export const registerLensVisualizationsUpdateAPIRoute: RegisterAPIRouteFn = (
return boomify(error); // forward unknown error
}
-
- return res.ok({ body: result.item });
}
);
};
diff --git a/x-pack/platform/plugins/shared/lens/common/content_management/latest.ts b/x-pack/platform/plugins/shared/lens/server/api/schema.ts
similarity index 88%
rename from x-pack/platform/plugins/shared/lens/common/content_management/latest.ts
rename to x-pack/platform/plugins/shared/lens/server/api/schema.ts
index ab3f7581b2043..8a8b2475c1c0f 100644
--- a/x-pack/platform/plugins/shared/lens/common/content_management/latest.ts
+++ b/x-pack/platform/plugins/shared/lens/server/api/schema.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export type * from './v1';
+export * from './routes/schema';
diff --git a/x-pack/platform/plugins/shared/lens/server/api/types.ts b/x-pack/platform/plugins/shared/lens/server/api/types.ts
index 68ff2865183b6..55fa468634cc6 100644
--- a/x-pack/platform/plugins/shared/lens/server/api/types.ts
+++ b/x-pack/platform/plugins/shared/lens/server/api/types.ts
@@ -9,6 +9,8 @@ import { HttpServiceSetup, Logger, RequestHandlerContext } from '@kbn/core/serve
import { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
import { VersionedRouter } from '@kbn/core-http-server';
+export type * from './routes/types';
+
export interface RegisterAPIRoutesArgs {
http: HttpServiceSetup;
contentManagement: ContentManagementServerSetup;
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/cm_services.ts b/x-pack/platform/plugins/shared/lens/server/content_management/cm_services.ts
deleted file mode 100644
index 74cb11396c764..0000000000000
--- a/x-pack/platform/plugins/shared/lens/server/content_management/cm_services.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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 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/x-pack/platform/plugins/shared/lens/server/content_management/index.ts b/x-pack/platform/plugins/shared/lens/server/content_management/index.ts
index f93ea0da74723..67fbe4f3a7a1d 100644
--- a/x-pack/platform/plugins/shared/lens/server/content_management/index.ts
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/index.ts
@@ -6,3 +6,9 @@
*/
export { LensStorage } from './lens_storage';
+export { servicesDefinitions } from './services';
+export * from './v1/schema/utils';
+
+export * from './latest';
+
+export type * as LensV1 from './v1';
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/latest.ts b/x-pack/platform/plugins/shared/lens/server/content_management/latest.ts
new file mode 100644
index 0000000000000..87c4b6b9a609a
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/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.
+ */
+
+// When we introduce a new version we need to export a union of all the version schemas
+export * from './v1';
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/lens_storage.ts b/x-pack/platform/plugins/shared/lens/server/content_management/lens_storage.ts
index 41a0b0953a779..0884a1d8ca29b 100644
--- a/x-pack/platform/plugins/shared/lens/server/content_management/lens_storage.ts
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/lens_storage.ts
@@ -4,100 +4,219 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
import Boom from '@hapi/boom';
-import type { SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
-import type { StorageContext } from '@kbn/content-management-plugin/server';
-import { SOContentStorage, tagsToFindOptions } from '@kbn/content-management-utils';
-import type { SavedObject, SavedObjectReference } from '@kbn/core-saved-objects-api-server';
+
import type { Logger } from '@kbn/logging';
+import type {
+ SavedObject,
+ SavedObjectsFindOptions,
+ SavedObjectsFindResult,
+} from '@kbn/core-saved-objects-api-server';
+import type { StorageContext } from '@kbn/content-management-plugin/server';
+import { SOContentStorage, SOWithMetadata, tagsToFindOptions } from '@kbn/content-management-utils';
-import {
- CONTENT_ID,
- type LensCrudTypes,
- type LensSavedObject,
- type LensSavedObjectAttributes,
- type PartialLensSavedObject,
-} from '../../common/content_management';
-import { cmServicesDefinition } from './cm_services';
+import { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common';
+import { LENS_CONTENT_TYPE } from '../../common/constants';
+import type {
+ LensAttributes,
+ LensGetOut,
+ LensSearchIn,
+ LensUpdateIn,
+ LensUpdateOut,
+ LensCrud,
+ LensCreateOut,
+ LensSearchOut,
+ LensSavedObject,
+ LensCreateIn,
+} from './latest';
+import { servicesDefinitions } from './services';
-const searchArgsToSOFindOptions = (args: LensCrudTypes['SearchIn']): SavedObjectsFindOptions => {
+const searchArgsToSOFindOptions = (args: LensSearchIn): SavedObjectsFindOptions => {
const { query, contentTypeId, options } = args;
+ // Shared schema type allows string here, need to align manually
+ const searchFields = (options?.searchFields
+ ? Array.isArray(options.searchFields)
+ ? options.searchFields
+ : [options.searchFields]
+ : null) ?? ['title^3', 'description'];
+
return {
type: contentTypeId,
- searchFields: ['title^3', 'description'],
- fields: ['description', 'title'],
search: query.text,
perPage: query.limit,
page: query.cursor ? +query.cursor : undefined,
defaultSearchOperator: 'AND',
...options,
...tagsToFindOptions(query.tags),
- };
+ searchFields,
+ } satisfies SavedObjectsFindOptions;
};
-const savedObjectClientFromRequest = async (ctx: StorageContext) => {
- if (!ctx.requestHandlerContext) {
- throw new Error('Storage context.requestHandlerContext missing.');
+export class LensStorage extends SOContentStorage {
+ constructor(params: { logger: Logger; throwOnResultValidationError: boolean }) {
+ super({
+ savedObjectType: LENS_CONTENT_TYPE,
+ cmServicesDefinition: servicesDefinitions,
+ searchArgsToSOFindOptions,
+ enableMSearch: true,
+ allowedSavedObjectAttributes: [
+ 'title',
+ 'description',
+ 'visualizationType',
+ 'version',
+ 'state',
+ ],
+ logger: params.logger,
+ throwOnResultValidationError: params.throwOnResultValidationError,
+ });
+
+ this.mSearch!.toItemResult = (ctx: StorageContext, savedObject: SavedObjectsFindResult) => {
+ const transforms = ctx.utils.getTransforms(servicesDefinitions);
+ const { attributes, ...rest } = this.savedObjectToItem(
+ // TODO: Fix this typing, this is not true we don't necessarily have all the Lens attributes
+ savedObject as SavedObject
+ ) as SOWithMetadata;
+ const commonContentItem: UserContentCommonSchema = {
+ updatedAt: '', // type misalignment
+ ...rest,
+ attributes: {
+ title: attributes.title,
+ description: attributes.description ?? '',
+ },
+ };
+
+ const validationError = transforms.mSearch.out.result.validate(commonContentItem);
+
+ if (validationError) {
+ if (this.throwOnResultValidationError) {
+ throw Boom.badRequest(`Invalid response. ${validationError.message}`);
+ } else {
+ this.logger.warn(`Invalid response. ${validationError.message}`);
+ }
+ }
+
+ // TODO: Fix this typing, this expects a full item attributes but only has title and description
+ return commonContentItem as LensSavedObject;
+ };
}
- const { savedObjects } = await ctx.requestHandlerContext.core;
- return savedObjects.client;
-};
+ async get(ctx: StorageContext, id: string): Promise {
+ const soClient = await LensStorage.getSOClientFromRequest(ctx);
-type PartialSavedObject = Omit>, 'references'> & {
- references: SavedObjectReference[] | undefined;
-};
+ // Save data in DB
+ const {
+ saved_object: savedObject,
+ alias_purpose: aliasPurpose,
+ alias_target_id: aliasTargetId,
+ outcome,
+ } = await soClient.resolve(LENS_CONTENT_TYPE, id);
-function savedObjectToLensSavedObject(
- savedObject:
- | SavedObject
- | PartialSavedObject
-): LensSavedObject | PartialLensSavedObject {
- const {
- id,
- type,
- updated_at: updatedAt,
- created_at: createdAt,
- attributes: { title, description, state, visualizationType },
- references,
- error,
- namespaces,
- } = savedObject;
+ const response: LensGetOut = {
+ item: this.savedObjectToItem(savedObject),
+ meta: {
+ aliasPurpose,
+ aliasTargetId,
+ outcome,
+ },
+ };
- return {
- id,
- type,
- updatedAt,
- createdAt,
- attributes: {
- title,
- description,
- visualizationType,
- state,
- },
- references,
- error,
- namespaces,
- };
-}
+ const itemVersion = response.item.attributes.version ?? 0;
+ const transforms = ctx.utils.getTransforms(servicesDefinitions);
-export class LensStorage extends SOContentStorage {
- constructor(
- private params: {
- logger: Logger;
- throwOnResultValidationError: boolean;
+ // transform item from given version to latest version
+ const { value, error: resultError } = transforms.get.out.result.down(
+ response,
+ itemVersion,
+ { validate: false } // validation is done after transform below
+ );
+
+ if (resultError) {
+ throw Boom.badRequest(`Transform error. ${resultError.message}`);
}
- ) {
- super({
- savedObjectType: CONTENT_ID,
- cmServicesDefinition,
- searchArgsToSOFindOptions,
- enableMSearch: true,
- allowedSavedObjectAttributes: ['title', 'description', 'visualizationType', 'state'],
- logger: params.logger,
- throwOnResultValidationError: params.throwOnResultValidationError,
- });
+
+ const validationError = transforms.get.out.result.validate(value);
+
+ if (validationError) {
+ if (this.throwOnResultValidationError) {
+ throw Boom.badRequest(`Invalid response. ${validationError.message}`);
+ } else {
+ this.logger.warn(`Invalid response. ${validationError.message}`);
+ }
+ }
+
+ return value;
+ }
+
+ async create(
+ ctx: StorageContext,
+ data: LensCreateIn['data'],
+ options: LensCreateIn['options'] = {}
+ ): Promise {
+ const transforms = ctx.utils.getTransforms(servicesDefinitions);
+ const soClient = await SOContentStorage.getSOClientFromRequest(ctx);
+
+ const itemVersion = data.version ?? 0; // Check that this always has a version
+
+ // transform data from given version to latest version
+ const { value: dataToLatest, error: dataError } = transforms.create.in.data.down<
+ LensCreateIn['data'],
+ LensCreateIn['data']
+ >(data, itemVersion);
+
+ if (dataError) {
+ throw Boom.badRequest(`Invalid data. ${dataError.message}`);
+ }
+
+ // transform options from given version to latest version
+ const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.down<
+ LensCreateIn['options'],
+ LensCreateIn['options']
+ >(options, itemVersion);
+
+ if (optionsError) {
+ throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
+ }
+
+ const createOptions = this.createArgsToSoCreateOptions(optionsToLatest ?? {});
+
+ // Save data in DB
+ const savedObject = await soClient.create(
+ LENS_CONTENT_TYPE,
+ dataToLatest,
+ createOptions
+ );
+
+ const result = {
+ item: this.savedObjectToItem(savedObject),
+ };
+
+ const validationError = transforms.create.out.result.validate(result);
+
+ if (validationError) {
+ if (this.throwOnResultValidationError) {
+ throw Boom.badRequest(`Invalid response. ${validationError.message}`);
+ } else {
+ this.logger.warn(`Invalid response. ${validationError.message}`);
+ }
+ }
+
+ // transform result from latest version to request version
+ const { value, error: resultError } = transforms.create.out.result.down<
+ LensCreateOut,
+ LensCreateOut
+ >(
+ result,
+ 'latest',
+ { validate: false } // validation is done above
+ );
+
+ if (resultError) {
+ throw Boom.badRequest(`Invalid response. ${resultError.message}`);
+ }
+
+ return value;
}
/**
@@ -107,64 +226,68 @@ export class LensStorage extends SOContentStorage {
async update(
ctx: StorageContext,
id: string,
- data: LensCrudTypes['UpdateIn']['data'],
- options: LensCrudTypes['UpdateOptions']
- ): Promise {
+ data: LensUpdateIn['data'],
+ options: LensUpdateIn['options']
+ ): Promise {
const {
utils: { getTransforms },
version: { request: requestVersion },
} = ctx;
- const transforms = getTransforms(cmServicesDefinition, requestVersion);
+ const transforms = getTransforms(servicesDefinitions, requestVersion);
+ const itemVersion = data.version ?? 0; // Check that this always has a version
+
+ // transform data from given version to latest version
+ const { value: dataToLatest, error: dataError } = transforms.update.in.data.down<
+ LensAttributes,
+ LensAttributes
+ >(data, itemVersion);
- // Validate input (data & options) & UP transform them to the latest version
- const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
- LensSavedObjectAttributes,
- LensSavedObjectAttributes
- >(data);
if (dataError) {
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
}
- const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
- LensCrudTypes['CreateOptions'],
- LensCrudTypes['CreateOptions']
- >(options);
+ // transform options from given version to latest version
+ const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.down<
+ LensUpdateIn['options'],
+ LensUpdateIn['options']
+ >(options, itemVersion);
+
if (optionsError) {
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
}
- // Save data in DB
- const soClient = await savedObjectClientFromRequest(ctx);
+ const soClient = await LensStorage.getSOClientFromRequest(ctx);
- // since we use create below this is to throw if SO id not found
- await soClient.get(CONTENT_ID, id);
+ // since we use create below this call is meant to throw if SO id not found
+ await soClient.get(LENS_CONTENT_TYPE, id);
- const savedObject = await soClient.create(CONTENT_ID, dataToLatest, {
+ const savedObject = await soClient.create(LENS_CONTENT_TYPE, dataToLatest, {
id,
overwrite: true,
...optionsToLatest,
});
const result = {
- item: savedObjectToLensSavedObject(savedObject),
+ item: this.savedObjectToItem(savedObject),
};
const validationError = transforms.update.out.result.validate(result);
+
if (validationError) {
- if (this.params.throwOnResultValidationError) {
+ if (this.throwOnResultValidationError) {
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
} else {
- this.params.logger.warn(`Invalid response. ${validationError.message}`);
+ this.logger.warn(`Invalid response. ${validationError.message}`);
}
}
- // Validate DB response and DOWN transform to the request version
+ // transform result from latest version to request version
const { value, error: resultError } = transforms.update.out.result.down<
- LensCrudTypes['UpdateOut'],
- LensCrudTypes['UpdateOut']
+ LensUpdateOut,
+ LensUpdateOut
>(
result,
- undefined, // do not override version
+ 'latest',
{ validate: false } // validation is done above
);
@@ -174,4 +297,70 @@ export class LensStorage extends SOContentStorage {
return value;
}
+
+ async search(
+ ctx: StorageContext,
+ query: LensSearchIn['query'],
+ options: LensSearchIn['options'] = {}
+ ): Promise {
+ const transforms = ctx.utils.getTransforms(servicesDefinitions);
+ const soClient = await SOContentStorage.getSOClientFromRequest(ctx);
+ const requestVersion = 'latest'; // this should eventually come from the request when there is a v2
+
+ // Validate and UP transform the options
+ const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
+ LensSearchIn['options'],
+ LensSearchIn['options']
+ >(options, requestVersion);
+ if (optionsError) {
+ throw Boom.badRequest(`Invalid payload. ${optionsError.message}`);
+ }
+
+ const soQuery: SavedObjectsFindOptions = this.searchArgsToSOFindOptions({
+ contentTypeId: LENS_CONTENT_TYPE,
+ query,
+ options: optionsToLatest,
+ });
+
+ // Execute the query in the DB
+ const soResponse = await soClient.find(soQuery);
+ const items = soResponse.saved_objects.map((so) => this.savedObjectToItem(so));
+
+ const transformedItems = items.map((item) => {
+ // transform item from given version to latest version
+ const { value: transformedItem, error: itemError } = transforms.search.out.result.down<
+ LensSavedObject,
+ LensSavedObject
+ >(
+ item,
+ item.attributes.version ?? 0,
+ { validate: false } // validation is done after transform below
+ );
+
+ if (itemError) {
+ throw Boom.badRequest(`Transform error. ${itemError.message}`);
+ }
+
+ return transformedItem;
+ });
+
+ const response = {
+ hits: transformedItems,
+ pagination: {
+ total: soResponse.total,
+ },
+ };
+
+ const validationError = transforms.search.out.result.validate(response);
+
+ if (validationError) {
+ if (this.throwOnResultValidationError) {
+ throw Boom.badRequest(`Invalid response. ${validationError.message}`);
+ } else {
+ this.logger.warn(`Invalid response. ${validationError.message}`);
+ }
+ }
+
+ return response;
+ }
}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/services.ts b/x-pack/platform/plugins/shared/lens/server/content_management/services.ts
new file mode 100644
index 0000000000000..a8de60e430778
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/services.ts
@@ -0,0 +1,14 @@
+/*
+ * 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, Version } from '@kbn/object-versioning';
+
+import { serviceDefinition as v1 } from './v1/service';
+
+export const servicesDefinitions: { [version: Version]: ContentManagementServicesDefinition } = {
+ 1: v1,
+};
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v0/index.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v0/index.ts
new file mode 100644
index 0000000000000..8c503964d466f
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v0/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 { lensItemAttributesSchemaV0, lensCreateRequestBodyDataSchemaV0 } from './schema';
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v0/schema.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v0/schema.ts
new file mode 100644
index 0000000000000..2a8ae66312087
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v0/schema.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { referencesSchema } from '@kbn/content-management-utils';
+
+/**
+ * Pre-existing Lens SO attributes (aka `v0`).
+ *
+ * We could still require handling see these attributes and should allow
+ * saving them as is with unknown version. The CM will eventually apply the transforms.
+ */
+export const lensItemAttributesSchemaV0 = schema.object(
+ {
+ title: schema.string(),
+ description: schema.maybe(schema.nullable(schema.string())),
+ visualizationType: schema.maybe(schema.string()),
+ state: schema.maybe(schema.any()),
+ uiStateJSON: schema.maybe(schema.string()),
+ visState: schema.maybe(schema.string()),
+ savedSearchRefName: schema.maybe(schema.string()),
+ },
+ { unknowns: 'ignore' }
+);
+
+/**
+ * Pre-existing Lens SO create body data (aka `v0`).
+ *
+ * We may require the ability to create a Lens SO with and old state.
+ */
+export const lensCreateRequestBodyDataSchemaV0 = lensItemAttributesSchemaV0.extends({
+ references: referencesSchema,
+});
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/cm_services.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/cm_services.ts
deleted file mode 100644
index afbd000805f82..0000000000000
--- a/x-pack/platform/plugins/shared/lens/server/content_management/v1/cm_services.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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);
-
-export const lensAttributesSchema = schema.object(
- {
- title: schema.string(),
- description: schema.maybe(schema.nullable(schema.string())),
- visualizationType: schema.maybe(schema.string()),
- state: schema.maybe(schema.any()),
- uiStateJSON: schema.maybe(schema.string()),
- visState: schema.maybe(schema.string()),
- savedSearchRefName: schema.maybe(schema.string()),
- },
- { unknowns: 'forbid' }
-);
-
-export const lensSavedObjectSchema = 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: lensAttributesSchema,
- references: referencesSchema,
- namespaces: schema.maybe(schema.arrayOf(schema.string())),
- originId: schema.maybe(schema.string()),
- },
- { unknowns: 'allow' }
-);
-
-const lensGetResultSchema = schema.object(
- {
- item: lensSavedObjectSchema,
- 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' }
-);
-
-export const lensCreateOptionsSchema = schema.object({
- overwrite: schema.maybe(schema.boolean()),
- references: schema.maybe(referencesSchema),
-});
-
-export const lensSearchOptionsSchema = schema.maybe(
- schema.object(
- {
- searchFields: schema.maybe(schema.arrayOf(schema.string())),
- types: schema.maybe(schema.arrayOf(schema.string())),
- },
- { unknowns: 'forbid' }
- )
-);
-
-const lensCreateResultSchema = schema.object(
- {
- item: lensSavedObjectSchema,
- },
- { unknowns: 'forbid' }
-);
-
-// Content management service definition.
-// We need it for BWC support between different versions of the content
-export const serviceDefinition: ServicesDefinition = {
- get: {
- out: {
- result: {
- schema: lensGetResultSchema,
- },
- },
- },
- create: {
- in: {
- data: {
- schema: lensAttributesSchema,
- },
- options: {
- schema: lensCreateOptionsSchema,
- },
- },
- out: {
- result: {
- schema: lensCreateResultSchema,
- },
- },
- },
- update: {
- in: {
- data: {
- schema: lensAttributesSchema,
- },
- options: {
- schema: lensCreateOptionsSchema,
- },
- },
- out: {
- result: {
- schema: lensCreateResultSchema,
- },
- },
- },
- search: {
- in: {
- options: {
- schema: lensSearchOptionsSchema,
- },
- },
- },
-};
diff --git a/x-pack/platform/plugins/shared/lens/server/api/constants.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/constants.ts
similarity index 57%
rename from x-pack/platform/plugins/shared/lens/server/api/constants.ts
rename to x-pack/platform/plugins/shared/lens/server/content_management/v1/constants.ts
index ef40c7044882e..22552421296f4 100644
--- a/x-pack/platform/plugins/shared/lens/server/api/constants.ts
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/constants.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
-export const PUBLIC_API_VERSION = '1';
-export const PUBLIC_API_CONTENT_MANAGEMENT_VERSION = 1;
-export const PUBLIC_API_PATH = '/api/lens';
-export const PUBLIC_API_ACCESS = 'internal';
+import { LENS_ITEM_VERSION_V1 } from '../../../common/content_management/constants';
+
+/**
+ * Lens CM Item Version `v1`
+ */
+export const LENS_ITEM_VERSION = LENS_ITEM_VERSION_V1;
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/index.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/index.ts
index e0be3d04393f7..303206c709a42 100644
--- a/x-pack/platform/plugins/shared/lens/server/content_management/v1/index.ts
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/index.ts
@@ -5,4 +5,11 @@
* 2.0.
*/
-export * from './cm_services';
+export { LENS_ITEM_VERSION } from './constants';
+
+export * from './transforms';
+export * from './service';
+export * from './schema';
+export type * from './types';
+
+export type { LensSavedObjectV0, LensAttributesV0 } from './transforms';
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/common.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/common.ts
new file mode 100644
index 0000000000000..acdc86b5de398
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/common.ts
@@ -0,0 +1,112 @@
+/*
+ * 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 { savedObjectSchema } from '@kbn/content-management-utils';
+
+import { pickFromObjectSchema } from './utils';
+import { LENS_ITEM_VERSION } from '../constants';
+
+export const lensItemAttributesSchema = schema.object(
+ {
+ title: schema.string(),
+ description: schema.maybe(schema.string()),
+ visualizationType: schema.nullable(schema.string()),
+ state: schema.maybe(schema.any()),
+ // TODO make version required
+ version: schema.maybe(schema.literal(LENS_ITEM_VERSION)), // pin version explicitly
+ },
+ { unknowns: 'forbid' }
+);
+
+export const lensAPIStateSchema = schema.object(
+ {
+ isNewApiFormat: schema.literal(true), // pin this to validate CB transformations
+ },
+ { unknowns: 'allow' }
+);
+
+export const lensAPIAttributesSchema = schema.object(
+ {
+ ...lensItemAttributesSchema.getPropSchemas(),
+ state: lensAPIStateSchema,
+ },
+ { unknowns: 'forbid' }
+);
+
+/**
+ * The underlying SO type used to store Lens state in Content Management.
+ *
+ * Only used in lens server-side Content Management.
+ */
+export const lensSavedObjectSchema = savedObjectSchema(lensItemAttributesSchema);
+
+/**
+ * The common SO type used for mSearch items.
+ */
+export const lensCommonSavedObjectSchema = savedObjectSchema(
+ schema.object(
+ {
+ ...pickFromObjectSchema(lensItemAttributesSchema.getPropSchemas(), ['title', 'description']),
+ },
+ { unknowns: 'forbid' }
+ )
+);
+
+/**
+ * The Lens item data returned from the server
+ */
+export const lensItemSchema = schema.object(
+ {
+ ...pickFromObjectSchema(lensSavedObjectSchema.getPropSchemas(), ['id', 'references']),
+ // Spread attributes at root
+ ...lensSavedObjectSchema.getPropSchemas().attributes.getPropSchemas(),
+ },
+ { unknowns: 'forbid' }
+);
+
+/**
+ * The Lens item data returned from the server
+ */
+export const lensAPIConfigSchema = schema.object(
+ {
+ // TODO flatten this with new CB shape
+ ...pickFromObjectSchema(lensSavedObjectSchema.getPropSchemas(), ['id', 'references']),
+ // Spread attributes at root
+ ...lensAPIAttributesSchema.getPropSchemas(),
+ },
+ { unknowns: 'forbid' }
+);
+
+/**
+ * The Lens item meta returned from the server
+ */
+export const lensItemMetaSchema = schema.object(
+ {
+ ...pickFromObjectSchema(lensSavedObjectSchema.getPropSchemas(), [
+ 'type',
+ 'createdAt',
+ 'updatedAt',
+ 'createdBy',
+ 'updatedBy',
+ 'originId', // maybe??
+ 'managed',
+ ]),
+ },
+ { unknowns: 'forbid' }
+);
+
+/**
+ * The Lens response item returned from the server
+ */
+export const lensResponseItemSchema = schema.object(
+ {
+ data: lensAPIConfigSchema,
+ meta: lensItemMetaSchema,
+ },
+ { unknowns: 'forbid' }
+);
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/create.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/create.ts
new file mode 100644
index 0000000000000..ed541fe1760d2
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/create.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { createOptionsSchemas, createResultSchema } from '@kbn/content-management-utils';
+
+import { lensItemAttributesSchemaV0 as lensItemAttributesSchemaV0 } from '../../v0/schema';
+import { lensItemAttributesSchema, lensSavedObjectSchema } from './common';
+import { pickFromObjectSchema } from './utils';
+
+export const lensCMCreateOptionsSchema = schema.object(
+ {
+ ...pickFromObjectSchema(createOptionsSchemas, ['overwrite', 'references']),
+ },
+ { unknowns: 'forbid' }
+);
+
+export const lensCMCreateBodySchema = schema.object(
+ {
+ options: lensCMCreateOptionsSchema,
+ // Permit passing old SO attributes on create
+ data: schema.oneOf([lensItemAttributesSchema, lensItemAttributesSchemaV0]),
+ },
+ {
+ unknowns: 'forbid',
+ }
+);
+
+export const lensCMCreateResultSchema = createResultSchema(lensSavedObjectSchema);
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/get.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/get.ts
new file mode 100644
index 0000000000000..8014134cae56d
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/get.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { objectTypeToGetResultSchema } from '@kbn/content-management-utils';
+
+import { lensSavedObjectSchema } from './common';
+
+export const lensCMGetResultSchema = objectTypeToGetResultSchema(lensSavedObjectSchema);
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/index.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/index.ts
new file mode 100644
index 0000000000000..c3f465f0f7ba3
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/index.ts
@@ -0,0 +1,13 @@
+/*
+ * 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 * from './common';
+export * from './get';
+export * from './create';
+export * from './update';
+export * from './search';
+export * from './m_search';
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/m_search.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/m_search.ts
new file mode 100644
index 0000000000000..2630ae86abef7
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/m_search.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.
+ */
+
+import { lensCommonSavedObjectSchema } from './common';
+
+export const lensCMMSearchResultSchema = lensCommonSavedObjectSchema;
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/search.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/search.ts
new file mode 100644
index 0000000000000..2ebda2bd32102
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/search.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { searchOptionsSchemas, searchResultSchema } from '@kbn/content-management-utils';
+
+import { lensSavedObjectSchema } from './common';
+import { pickFromObjectSchema } from './utils';
+
+export const lensCMSearchOptionsSchema = schema.object(
+ {
+ // TODO: add support for more search options
+ ...pickFromObjectSchema(searchOptionsSchemas, ['fields', 'searchFields']),
+ },
+ {
+ unknowns: 'forbid',
+ }
+);
+
+export const lensCMSearchResultSchema = searchResultSchema(lensSavedObjectSchema);
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/update.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/update.ts
new file mode 100644
index 0000000000000..8d195f03c8067
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/update.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 { omit } from 'lodash';
+
+import { schema } from '@kbn/config-schema';
+import { createResultSchema, updateOptionsSchema } from '@kbn/content-management-utils';
+
+import { lensItemAttributesSchema, lensSavedObjectSchema } from './common';
+import { pickFromObjectSchema } from './utils';
+
+export const lensCMUpdateOptionsSchema = schema.object(
+ {
+ ...pickFromObjectSchema(
+ omit(updateOptionsSchema, 'upsert'), // `upsert` is not a schema type
+ ['references']
+ ),
+ },
+ { unknowns: 'forbid' }
+);
+
+export const lensCMUpdateBodySchema = schema.object(
+ {
+ options: lensCMUpdateOptionsSchema,
+ data: lensItemAttributesSchema,
+ },
+ {
+ unknowns: 'forbid',
+ }
+);
+
+export const lensCMUpdateResultSchema = createResultSchema(lensSavedObjectSchema);
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/utils.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/utils.ts
new file mode 100644
index 0000000000000..b9f9892dfd2a6
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/schema/utils.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { pick } from 'lodash';
+
+import { Props } from '@kbn/config-schema';
+import { Assign } from 'utility-types';
+
+/**
+ * Picks a subset of props from base schema definition
+ *
+ * TODO: move this to `@kbn/config-schema`
+ */
+export function pickFromObjectSchema(
+ schema: T,
+ keys: K[]
+): Assign<{}, Pick> {
+ // Note: Assign type is required to prevent omitted key pollution on spread
+
+ // lodash.pick types do not infer the object type to enforce keyof T
+ return pick(schema, keys);
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/service.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/service.ts
new file mode 100644
index 0000000000000..1593818dece8d
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/service.ts
@@ -0,0 +1,89 @@
+/*
+ * 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 } from '@kbn/object-versioning';
+
+import {
+ lensCMGetResultSchema,
+ lensItemAttributesSchema,
+ lensCMCreateOptionsSchema,
+ lensCMCreateResultSchema,
+ lensCMSearchOptionsSchema,
+ lensCMUpdateResultSchema,
+ lensCMSearchResultSchema,
+ lensCMUpdateOptionsSchema,
+} from './schema';
+import { transformToV1LensSavedObject, transformToV1LensItemAttributes } from './transforms';
+import { LensAttributes, LensGetOut, LensSavedObject } from './types';
+
+export const serviceDefinition = {
+ get: {
+ out: {
+ result: {
+ schema: lensCMGetResultSchema,
+ up: (result: LensGetOut) => {
+ return {
+ ...result,
+ item: transformToV1LensSavedObject(result.item),
+ } satisfies LensGetOut;
+ },
+ },
+ },
+ },
+ create: {
+ in: {
+ data: {
+ schema: lensItemAttributesSchema,
+ up: (attributes: LensAttributes) => {
+ return transformToV1LensItemAttributes(attributes);
+ },
+ },
+ options: {
+ schema: lensCMCreateOptionsSchema,
+ },
+ },
+ out: {
+ result: {
+ schema: lensCMCreateResultSchema,
+ },
+ },
+ },
+ update: {
+ in: {
+ data: {
+ schema: lensItemAttributesSchema,
+ up: (attributes: LensAttributes) => {
+ return transformToV1LensItemAttributes(attributes);
+ },
+ },
+ options: {
+ schema: lensCMUpdateOptionsSchema,
+ },
+ },
+ out: {
+ result: {
+ schema: lensCMUpdateResultSchema,
+ },
+ },
+ },
+ search: {
+ in: {
+ options: {
+ schema: lensCMSearchOptionsSchema,
+ },
+ },
+ out: {
+ result: {
+ schema: lensCMSearchResultSchema,
+ up: (item: LensSavedObject) => {
+ // apply v1 transform per item, items may have different versions
+ return transformToV1LensSavedObject(item);
+ },
+ },
+ },
+ },
+} satisfies ContentManagementServicesDefinition;
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/add_version.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/add_version.ts
new file mode 100644
index 0000000000000..c7e35a9686dde
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/add_version.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { LENS_ITEM_VERSION } from '../constants';
+import { LensAttributes } from '../types';
+
+export function addVersion(attributes: LensAttributes): LensAttributes {
+ return {
+ ...attributes,
+ version: LENS_ITEM_VERSION,
+ };
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/index.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/index.ts
new file mode 100644
index 0000000000000..239e9d4f376bc
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export type * from './types';
+export * from './transforms';
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/index.test.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/index.test.ts
new file mode 100644
index 0000000000000..5c89e482d4da3
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/index.test.ts
@@ -0,0 +1,73 @@
+/*
+ * 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 { convertToLegendStats } from '.';
+import { LensAttributes } from '../../types';
+import { convertPartitionToLegendStats } from './partition';
+import { convertXYToLegendStats } from './xy';
+
+jest.mock('./xy', () => ({
+ convertXYToLegendStats: jest.fn().mockReturnValue('new xyVisState'),
+}));
+jest.mock('./partition', () => ({
+ convertPartitionToLegendStats: jest.fn().mockReturnValue('new partitionVisState'),
+}));
+
+describe('Legend stat transforms', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return original attributes if no state', () => {
+ const attributes = {
+ state: undefined,
+ } as LensAttributes;
+ const result = convertToLegendStats(attributes);
+
+ expect(result).toBe(attributes);
+ });
+
+ it('should return original attributes for noop visualizationTypes', () => {
+ const attributes = {
+ state: {},
+ visualizationType: 'noop',
+ } as LensAttributes;
+ const result = convertToLegendStats(attributes);
+
+ expect(result).toBe(attributes);
+ });
+
+ it('should convert lnsXY attributes', () => {
+ const attributes = {
+ state: {
+ visualization: 'xyVisState',
+ },
+ visualizationType: 'lnsXY',
+ } as LensAttributes;
+ const result = convertToLegendStats(attributes);
+
+ expect(convertXYToLegendStats).toBeCalledWith('xyVisState');
+ expect(result.state).toMatchObject({
+ visualization: 'new xyVisState',
+ });
+ });
+
+ it('should convert lnsPie attributes', () => {
+ const attributes = {
+ state: {
+ visualization: 'partitionVisState',
+ },
+ visualizationType: 'lnsPie',
+ } as LensAttributes;
+ const result = convertToLegendStats(attributes);
+
+ expect(convertPartitionToLegendStats).toBeCalledWith('partitionVisState');
+ expect(result.state).toMatchObject({
+ visualization: 'new partitionVisState',
+ });
+ });
+});
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/index.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/index.ts
new file mode 100644
index 0000000000000..e832a5e301087
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/index.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { XYState } from '../../../../../public';
+import type { PieVisualizationState } from '../../../../../common/types';
+import type { LensAttributes } from '../../types';
+import {
+ convertPartitionToLegendStats,
+ type DeprecatedLegendValuePieVisualizationState,
+} from './partition';
+import { convertXYToLegendStats, type DeprecatedLegendValueXYState } from './xy';
+
+export function convertToLegendStats(attributes: LensAttributes): LensAttributes {
+ if (
+ !attributes.state ||
+ (attributes.visualizationType !== 'lnsXY' && attributes.visualizationType !== 'lnsPie')
+ ) {
+ return attributes;
+ }
+
+ const newVisualizationState = getUpdatedVisualizationState(
+ attributes.visualizationType,
+ attributes.state as Record
+ );
+
+ return {
+ ...attributes,
+ state: {
+ ...(attributes.state as Record),
+ visualization: newVisualizationState,
+ },
+ };
+}
+
+export function getUpdatedVisualizationState(
+ visualizationType: LensAttributes['visualizationType'],
+ state: LensAttributes['state'] & { visualization?: unknown }
+): LensAttributes['state'] {
+ if (visualizationType === 'lnsXY' && state?.visualization) {
+ const visState = state.visualization as XYState | DeprecatedLegendValueXYState;
+ return convertXYToLegendStats(visState);
+ }
+
+ if (visualizationType === 'lnsPie' && state?.visualization) {
+ const visState = state.visualization as
+ | PieVisualizationState
+ | DeprecatedLegendValuePieVisualizationState;
+ return convertPartitionToLegendStats(visState);
+ }
+
+ return state.visualization;
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/partition.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/partition.ts
new file mode 100644
index 0000000000000..f813d0ad19f3a
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/partition.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { LegendValue } from '@elastic/charts';
+
+import { PieLayerState, PieVisualizationState } from '../../../../../common/types';
+
+/** @deprecated */
+type DeprecatedLegendValueLayer = PieLayerState & {
+ showValuesInLegend?: boolean;
+};
+
+/**
+ * Old color mapping state meant for type safety during runtime migrations of old configurations
+ *
+ * @deprecated
+ */
+export type DeprecatedLegendValuePieVisualizationState = Omit & {
+ layers: DeprecatedLegendValueLayer[];
+};
+
+export function convertPartitionToLegendStats(
+ state: DeprecatedLegendValuePieVisualizationState | PieVisualizationState
+) {
+ state.layers.forEach((l) => {
+ if ('showValuesInLegend' in l) {
+ l.legendStats = [
+ ...new Set([
+ ...(l.showValuesInLegend ? [LegendValue.Value] : []),
+ ...(l.legendStats ?? []),
+ ]),
+ ];
+ }
+ delete (l as DeprecatedLegendValueLayer).showValuesInLegend;
+ });
+
+ return state;
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/types.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/types.ts
new file mode 100644
index 0000000000000..3c1338772d318
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/types.ts
@@ -0,0 +1,13 @@
+/*
+ * 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 { DeprecatedLegendValuePieVisualizationState } from './partition';
+import { DeprecatedLegendValueXYState } from './xy';
+
+export type DeprecatedLegendValueState =
+ | DeprecatedLegendValueXYState
+ | DeprecatedLegendValuePieVisualizationState;
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/xy.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/xy.ts
new file mode 100644
index 0000000000000..c815cb01a5358
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/legend_stats/xy.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { LegendValue } from '@elastic/charts';
+
+import type { XYState } from '../../../../../public/visualizations/xy/types';
+
+/**
+ * Old color mapping state meant for type safety during runtime migrations of old configurations
+ *
+ * @deprecated
+ */
+export interface DeprecatedLegendValueXYState extends XYState {
+ valuesInLegend?: boolean;
+}
+
+export function convertXYToLegendStats(state: DeprecatedLegendValueXYState | XYState): XYState {
+ if ('valuesInLegend' in state) {
+ const valuesInLegend = state.valuesInLegend;
+ delete state.valuesInLegend;
+
+ const result: XYState = {
+ ...state,
+ legend: {
+ ...state.legend,
+ legendStats: [
+ ...new Set([
+ ...(valuesInLegend ? [LegendValue.CurrentAndLastValue] : []),
+ ...(state.legend.legendStats ?? []),
+ ]),
+ ],
+ },
+ };
+
+ return result;
+ }
+ return state;
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/converter.test.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/converter.test.ts
new file mode 100644
index 0000000000000..2be0c0278349c
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/converter.test.ts
@@ -0,0 +1,435 @@
+/*
+ * 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 { ColorMapping } from '@kbn/coloring';
+import { SerializedRangeKey } from '@kbn/data-plugin/common/search';
+
+import type { DataType, GenericIndexPatternColumn } from '../../../../../../public';
+import type { DeprecatedColorMappingConfig } from './types';
+import { convertToRawColorMappings, isDeprecatedColorMapping } from './converter';
+
+type OldAssignment = DeprecatedColorMappingConfig['assignments'][number];
+type OldRule = OldAssignment['rule'];
+type OldSpecialRule = DeprecatedColorMappingConfig['specialAssignments'][number]['rule'];
+
+const baseConfig = {
+ assignments: [],
+ specialAssignments: [],
+ paletteId: 'default',
+ colorMode: {
+ type: 'categorical',
+ },
+} satisfies DeprecatedColorMappingConfig | ColorMapping.Config;
+
+const getOldColorMapping = (
+ rules: OldRule[],
+ specialRules: OldSpecialRule[] = []
+): DeprecatedColorMappingConfig => ({
+ assignments: rules.map((rule, i) => ({
+ rule,
+ color: {
+ type: 'categorical',
+ paletteId: 'default',
+ colorIndex: i,
+ },
+ touched: false,
+ })),
+ specialAssignments: specialRules.map((rule) => ({
+ rule,
+ color: {
+ type: 'loop',
+ },
+ touched: false,
+ })),
+ paletteId: 'default',
+ colorMode: {
+ type: 'categorical',
+ },
+});
+
+function buildOldColorMapping(
+ rules: OldRule[],
+ specialRules: OldSpecialRule[] = []
+): DeprecatedColorMappingConfig {
+ return getOldColorMapping(rules, specialRules);
+}
+
+describe('converter', () => {
+ describe('#convertToRawColorMappings', () => {
+ it('should convert config with no assignments', () => {
+ const oldConfig = buildOldColorMapping([]);
+ const newConfig = convertToRawColorMappings(oldConfig);
+ expect(newConfig.assignments).toHaveLength(0);
+ });
+
+ it('should keep top-level config', () => {
+ const oldConfig = buildOldColorMapping([]);
+ const newConfig = convertToRawColorMappings(oldConfig);
+ expect(newConfig).toMatchObject({
+ paletteId: 'default',
+ colorMode: {
+ type: 'categorical',
+ },
+ });
+ });
+
+ describe('type - auto', () => {
+ it('should convert single auto rule', () => {
+ const oldConfig = buildOldColorMapping([{ type: 'auto' }]);
+ const newConfig = convertToRawColorMappings(oldConfig);
+ expect(newConfig.assignments).toHaveLength(1);
+ expect(newConfig.assignments[0].color).toBeDefined();
+ expect(newConfig.assignments[0].rules).toEqual([]);
+ });
+
+ it('should convert multiple auto rule', () => {
+ const oldConfig = buildOldColorMapping([
+ { type: 'auto' },
+ { type: 'matchExactly', values: [] },
+ { type: 'auto' },
+ ]);
+ const newConfig = convertToRawColorMappings(oldConfig);
+ expect(newConfig.assignments).toHaveLength(3);
+ expect(newConfig.assignments[0].rules).toEqual([]);
+ expect(newConfig.assignments[2].rules).toEqual([]);
+ });
+ });
+
+ describe('type - matchExactly', () => {
+ type ExpectedRule = Partial;
+ interface ExpectedRulesByType {
+ types: Array;
+ expectedRule: ExpectedRule;
+ }
+ type MatchExactlyTestCase = [
+ oldStringValue: string | string[],
+ defaultExpectedRule: ExpectedRule,
+ expectedRulesByType: ExpectedRulesByType[]
+ ];
+
+ const buildOldColorMappingFromValues = (values: Array) =>
+ buildOldColorMapping([{ type: 'matchExactly', values }]);
+
+ it('should handle missing column', () => {
+ const oldConfig = buildOldColorMapping([{ type: 'matchExactly', values: ['test'] }]);
+ const newConfig = convertToRawColorMappings(oldConfig, undefined);
+ expect(newConfig.assignments).toHaveLength(1);
+ expect(newConfig.assignments[0].rules).toHaveLength(1);
+ expect(newConfig.assignments[0].rules[0]).toEqual({
+ type: 'match',
+ pattern: 'test',
+ matchEntireWord: true,
+ matchCase: true,
+ });
+ });
+
+ describe('multi_terms', () => {
+ it('should convert array of string values as MultiFieldKey', () => {
+ const values: string[] = ['some-string', '123', '0', '1', '1744261200000', '__other__'];
+ const oldConfig = buildOldColorMappingFromValues([values]);
+ const newConfig = convertToRawColorMappings(oldConfig, {});
+ const rule = newConfig.assignments[0].rules[0];
+
+ expect(rule).toEqual({
+ type: 'raw',
+ value: {
+ keys: ['some-string', '123', '0', '1', '1744261200000', '__other__'],
+ type: 'multiFieldKey',
+ },
+ });
+ });
+
+ it('should convert array of strings in multi_terms as MultiFieldKey', () => {
+ const oldConfig = buildOldColorMappingFromValues([['some-string']]);
+ const newConfig = convertToRawColorMappings(oldConfig, {
+ fieldType: 'multi_terms',
+ });
+ const rule = newConfig.assignments[0].rules[0];
+
+ expect(rule).toEqual({
+ type: 'raw',
+ value: {
+ keys: ['some-string'],
+ type: 'multiFieldKey',
+ },
+ });
+ });
+
+ it('should convert single string as basic match even in multi_terms column', () => {
+ const oldConfig = buildOldColorMappingFromValues(['some-string']);
+ const newConfig = convertToRawColorMappings(oldConfig, {
+ fieldType: 'multi_terms',
+ });
+ const rule = newConfig.assignments[0].rules[0];
+
+ expect(rule).toEqual({
+ type: 'match',
+ pattern: 'some-string',
+ matchEntireWord: true,
+ matchCase: true,
+ });
+ });
+ });
+
+ describe('range', () => {
+ it.each<[rangeString: string, expectedRange: Pick]>([
+ ['from:0,to:1000', { from: 0, to: 1000 }],
+ ['from:-1000,to:1000', { from: -1000, to: 1000 }],
+ ['from:-1000,to:0', { from: -1000, to: 0 }],
+ ['from:1000,to:undefined', { from: 1000, to: null }],
+ ['from:undefined,to:1000', { from: null, to: 1000 }],
+ ['from:undefined,to:undefined', { from: null, to: null }],
+ ])('should convert range string %j to RangeKey', (rangeString, expectedRange) => {
+ const oldConfig = buildOldColorMappingFromValues([rangeString]);
+ const newConfig = convertToRawColorMappings(oldConfig, {
+ fieldType: 'range',
+ });
+ const rule = newConfig.assignments[0].rules[0];
+
+ expect(rule).toEqual({
+ type: 'raw',
+ value: {
+ ...expectedRange,
+ type: 'rangeKey',
+ ranges: [],
+ },
+ });
+ });
+
+ it('should convert non-range string to match', () => {
+ const oldConfig = buildOldColorMappingFromValues(['not-a-range']);
+ const newConfig = convertToRawColorMappings(oldConfig, {
+ fieldType: 'range',
+ });
+ const rule = newConfig.assignments[0].rules[0];
+
+ expect(rule).toEqual({
+ type: 'match',
+ pattern: 'not-a-range',
+ matchEntireWord: true,
+ matchCase: true,
+ });
+ });
+ });
+
+ describe.each([
+ 'number',
+ 'boolean',
+ 'date',
+ 'string',
+ 'ip',
+ undefined, // other
+ ])('Column dataType - %s', (dataType) => {
+ const column: Partial = { dataType };
+
+ it.each([
+ [
+ '123.456',
+ {
+ type: 'raw',
+ value: 123.456,
+ },
+ [
+ { types: ['string', 'ip'], expectedRule: { type: 'raw', value: '123.456' } },
+ {
+ types: ['undefined', 'boolean'],
+ expectedRule: { type: 'match', pattern: '123.456' },
+ },
+ ],
+ ],
+ [
+ '1744261200000',
+ {
+ type: 'raw',
+ value: 1744261200000,
+ },
+ [
+ { types: ['string', 'ip'], expectedRule: { type: 'raw', value: '1744261200000' } },
+ {
+ types: ['undefined', 'boolean'],
+ expectedRule: { type: 'match', pattern: '1744261200000' },
+ },
+ ],
+ ],
+ [
+ '__other__',
+ {
+ type: 'raw',
+ value: '__other__',
+ },
+ [{ types: ['undefined'], expectedRule: { type: 'match', pattern: '__other__' } }],
+ ],
+ [
+ 'some-string',
+ {
+ type: 'raw',
+ value: 'some-string',
+ },
+ [
+ {
+ types: ['undefined', 'number', 'boolean', 'date'],
+ expectedRule: { type: 'match', pattern: 'some-string' },
+ },
+ ],
+ ],
+ [
+ 'false',
+ {
+ type: 'raw',
+ value: 'false',
+ },
+ [
+ {
+ types: ['undefined', 'number', 'date'],
+ expectedRule: { type: 'match', pattern: 'false' },
+ },
+ ],
+ ],
+ [
+ 'true',
+ {
+ type: 'raw',
+ value: 'true',
+ },
+ [
+ {
+ types: ['undefined', 'number', 'date'],
+ expectedRule: { type: 'match', pattern: 'true' },
+ },
+ ],
+ ],
+ [
+ '0', // false
+ {
+ type: 'raw',
+ value: 0,
+ },
+ [
+ { types: ['string', 'ip'], expectedRule: { type: 'raw', value: '0' } },
+ { types: ['undefined'], expectedRule: { type: 'match', pattern: '0' } },
+ ],
+ ],
+ [
+ '1', // true
+ {
+ type: 'raw',
+ value: 1,
+ },
+ [
+ { types: ['string', 'ip'], expectedRule: { type: 'raw', value: '1' } },
+ { types: ['undefined'], expectedRule: { type: 'match', pattern: '1' } },
+ ],
+ ],
+ [
+ '127.0.0.1',
+ {
+ type: 'raw',
+ value: '127.0.0.1',
+ },
+ [
+ {
+ types: ['undefined', 'number', 'boolean', 'date'],
+ expectedRule: { type: 'match', pattern: '127.0.0.1' },
+ },
+ ],
+ ],
+ ])('should correctly convert %j', (value, defaultExpectedRule, expectedRulesByType) => {
+ const oldConfig = buildOldColorMappingFromValues([value]);
+ const expectedRule =
+ expectedRulesByType.find((r) => r.types.includes(dataType ?? 'undefined'))
+ ?.expectedRule ?? defaultExpectedRule;
+ const newConfig = convertToRawColorMappings(oldConfig, { ...column });
+ const rule = newConfig.assignments[0].rules[0];
+
+ if (expectedRule.type === 'match') {
+ // decorate match type with default constants
+ expectedRule.matchEntireWord = true;
+ expectedRule.matchCase = true;
+ }
+
+ expect(rule).toEqual(expectedRule);
+ });
+ });
+ });
+ });
+
+ describe('#isDeprecatedColorMapping', () => {
+ const baseAssignment = {
+ color: {
+ type: 'categorical',
+ paletteId: 'default',
+ colorIndex: 3,
+ },
+ touched: false,
+ } satisfies Omit;
+
+ it('should return true if assignments.rule exists', () => {
+ const isDeprecated = isDeprecatedColorMapping({
+ ...baseConfig,
+ assignments: [
+ {
+ ...baseAssignment,
+ rule: {
+ type: 'auto',
+ },
+ },
+ ],
+ });
+ expect(isDeprecated).toBe(true);
+ });
+
+ it('should return true if specialAssignments.rule exists', () => {
+ const isDeprecated = isDeprecatedColorMapping({
+ ...baseConfig,
+ specialAssignments: [
+ {
+ ...baseAssignment,
+ rule: {
+ type: 'other',
+ },
+ },
+ ],
+ });
+ expect(isDeprecated).toBe(true);
+ });
+
+ it('should return false if assignments.rule does not exist', () => {
+ const isDeprecated = isDeprecatedColorMapping({
+ ...baseConfig,
+ assignments: [
+ {
+ ...baseAssignment,
+ rules: [
+ {
+ type: 'match',
+ pattern: 'test',
+ },
+ ],
+ },
+ ],
+ });
+ expect(isDeprecated).toBe(false);
+ });
+
+ it('should return false if specialAssignments.rule does not exist', () => {
+ const isDeprecated = isDeprecatedColorMapping({
+ ...baseConfig,
+ specialAssignments: [
+ {
+ ...baseAssignment,
+ rules: [
+ {
+ type: 'other',
+ },
+ ],
+ },
+ ],
+ });
+ expect(isDeprecated).toBe(false);
+ });
+ });
+});
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/converter.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/converter.ts
new file mode 100644
index 0000000000000..96f3af4f51f18
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/converter.ts
@@ -0,0 +1,161 @@
+/*
+ * 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 { ColorMapping } from '@kbn/coloring';
+import { MultiFieldKey, RangeKey, SerializedValue } from '@kbn/data-plugin/common';
+
+import type { DeprecatedColorMappingConfig } from './types';
+import type { ColumnMeta } from './utils';
+
+/**
+ * Converts old stringified colorMapping configs to new raw value configs
+ */
+export function convertToRawColorMappings(
+ colorMapping: DeprecatedColorMappingConfig | ColorMapping.Config,
+ columnMeta?: ColumnMeta | null
+): ColorMapping.Config {
+ return {
+ ...colorMapping,
+ assignments: colorMapping.assignments.map((oldAssignment) => {
+ if (isValidColorMappingAssignment(oldAssignment)) return oldAssignment;
+ return convertColorMappingAssignment(oldAssignment, columnMeta);
+ }),
+ specialAssignments: colorMapping.specialAssignments.map((oldAssignment) => {
+ if (isValidColorMappingAssignment(oldAssignment)) return oldAssignment;
+ return {
+ color: oldAssignment.color,
+ touched: oldAssignment.touched,
+ rules: [oldAssignment.rule],
+ };
+ }),
+ };
+}
+
+function convertColorMappingAssignment(
+ oldAssignment: DeprecatedColorMappingConfig['assignments'][number],
+ columnMeta?: ColumnMeta | null
+): ColorMapping.Assignment {
+ return {
+ color: oldAssignment.color,
+ touched: oldAssignment.touched,
+ rules: convertColorMappingRule(oldAssignment.rule, columnMeta),
+ };
+}
+
+const NO_VALUE = Symbol('no-value');
+
+function convertColorMappingRule(
+ rule: DeprecatedColorMappingConfig['assignments'][number]['rule'],
+ columnMeta?: ColumnMeta | null
+): ColorMapping.ColorRule[] {
+ switch (rule.type) {
+ case 'auto':
+ return [];
+ case 'matchExactly':
+ return rule.values.map((value) => {
+ const rawValue = convertToRawValue(value, columnMeta);
+
+ if (rawValue !== NO_VALUE) {
+ return {
+ type: 'raw',
+ value: rawValue,
+ };
+ }
+
+ return {
+ type: 'match',
+ pattern: String(value),
+ matchEntireWord: true,
+ matchCase: true,
+ };
+ });
+
+ // Rules below not yet used, adding conversions for completeness
+ case 'matchExactlyCI':
+ return rule.values.map((value) => ({
+ type: 'match',
+ pattern: Array.isArray(value) ? value.join(' ') : value,
+ matchEntireWord: true,
+ matchCase: false,
+ }));
+ case 'regex':
+ return [{ type: rule.type, pattern: rule.values }];
+ case 'range':
+ default:
+ return [rule];
+ }
+}
+
+/**
+ * Attempts to convert the previously stringified raw values into their raw/serialized form
+ *
+ * Note: we use the `NO_VALUE` symbol to avoid collisions with falsy raw values
+ */
+function convertToRawValue(
+ value: string | string[],
+ columnMeta?: ColumnMeta | null
+): SerializedValue | symbol {
+ if (!columnMeta) return NO_VALUE;
+
+ // all array values are multi-term
+ if (columnMeta.fieldType === 'multi_terms' || Array.isArray(value)) {
+ if (typeof value === 'string') return NO_VALUE; // cannot assume this as multi-field
+ return new MultiFieldKey({ key: value }).serialize();
+ }
+
+ if (columnMeta.fieldType === 'range') {
+ return RangeKey.isRangeKeyString(value) ? RangeKey.fromString(value).serialize() : NO_VALUE;
+ }
+
+ switch (columnMeta.dataType) {
+ case 'boolean':
+ if (value === '__other__' || value === 'true' || value === 'false') return value; // bool could have __other__ as a string
+ if (value === '0' || value === '1') return Number(value);
+ break;
+ case 'number':
+ case 'date':
+ if (value === '__other__') return value; // numbers can have __other__ as a string
+ const numberValue = Number(value);
+ if (isFinite(numberValue)) return numberValue;
+ break;
+ case 'string':
+ case 'ip':
+ return value; // unable to distinguish manually added values
+ default:
+ return NO_VALUE; // treat all other other dataType as custom match string values
+ }
+ return NO_VALUE;
+}
+
+function isValidColorMappingAssignment<
+ T extends
+ | DeprecatedColorMappingConfig['assignments'][number]
+ | DeprecatedColorMappingConfig['specialAssignments'][number]
+ | ColorMapping.Config['assignments'][number]
+ | ColorMapping.Config['specialAssignments'][number]
+>(
+ assignment: T
+): assignment is Exclude<
+ T,
+ | DeprecatedColorMappingConfig['assignments'][number]
+ | DeprecatedColorMappingConfig['specialAssignments'][number]
+> {
+ return 'rules' in assignment;
+}
+
+export function isDeprecatedColorMapping<
+ T extends DeprecatedColorMappingConfig | ColorMapping.Config
+>(colorMapping?: T): colorMapping is Exclude {
+ if (!colorMapping) return false;
+ return Boolean(
+ colorMapping.assignments &&
+ (colorMapping.assignments.some((assignment) => !isValidColorMappingAssignment(assignment)) ||
+ colorMapping.specialAssignments.some(
+ (specialAssignment) => !isValidColorMappingAssignment(specialAssignment)
+ ))
+ );
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/index.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/index.ts
new file mode 100644
index 0000000000000..9e05cb0b43aa1
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/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 type { DeprecatedColorMappingConfig } from './types';
+export { convertToRawColorMappings, isDeprecatedColorMapping } from './converter';
+export { getColumnMetaFn } from './utils';
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/types.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/types.ts
new file mode 100644
index 0000000000000..be3c2dc239097
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/types.ts
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+/** @deprecated */
+interface DeprecatedColorMappingColorCode {
+ type: 'colorCode';
+ colorCode: string;
+}
+
+/** @deprecated */
+interface DeprecatedColorMappingCategoricalColor {
+ type: 'categorical';
+ paletteId: string;
+ colorIndex: number;
+}
+
+/** @deprecated */
+interface DeprecatedColorMappingGradientColor {
+ type: 'gradient';
+}
+
+/** @deprecated */
+interface DeprecatedColorMappingLoopColor {
+ type: 'loop';
+}
+
+/** @deprecated */
+interface DeprecatedColorMappingRuleAuto {
+ type: 'auto';
+}
+/** @deprecated */
+interface DeprecatedColorMappingRuleMatchExactly {
+ type: 'matchExactly';
+ values: Array;
+}
+/** @deprecated */
+interface DeprecatedColorMappingRuleMatchExactlyCI {
+ type: 'matchExactlyCI';
+ values: string[];
+}
+
+/** @deprecated */
+interface DeprecatedColorMappingRuleRange {
+ type: 'range';
+ min: number;
+ max: number;
+ minInclusive: boolean;
+ maxInclusive: boolean;
+}
+
+/** @deprecated */
+interface DeprecatedColorMappingRuleRegExp {
+ type: 'regex';
+ values: string;
+}
+
+/** @deprecated */
+interface DeprecatedColorMappingRuleOthers {
+ type: 'other';
+}
+
+/** @deprecated */
+interface DeprecatedColorMappingAssignment {
+ rule: R;
+ color: C;
+ touched: boolean;
+}
+
+/** @deprecated */
+interface DeprecatedColorMappingCategoricalColorMode {
+ type: 'categorical';
+}
+
+/** @deprecated */
+interface DeprecatedColorMappingGradientColorMode {
+ type: 'gradient';
+ steps: Array<
+ (DeprecatedColorMappingCategoricalColor | DeprecatedColorMappingColorCode) & {
+ touched: boolean;
+ }
+ >;
+ sort: 'asc' | 'desc';
+}
+
+/**
+ * Old color mapping state meant for type safety during runtime migrations of old configurations
+ *
+ * @deprecated Use `ColorMapping.Config`
+ */
+export interface DeprecatedColorMappingConfig {
+ paletteId: string;
+ colorMode: DeprecatedColorMappingCategoricalColorMode | DeprecatedColorMappingGradientColorMode;
+ assignments: Array<
+ DeprecatedColorMappingAssignment<
+ | DeprecatedColorMappingRuleAuto
+ | DeprecatedColorMappingRuleMatchExactly
+ | DeprecatedColorMappingRuleMatchExactlyCI
+ | DeprecatedColorMappingRuleRange
+ | DeprecatedColorMappingRuleRegExp,
+ | DeprecatedColorMappingCategoricalColor
+ | DeprecatedColorMappingColorCode
+ | DeprecatedColorMappingGradientColor
+ >
+ >;
+ specialAssignments: Array<
+ DeprecatedColorMappingAssignment<
+ DeprecatedColorMappingRuleOthers,
+ | DeprecatedColorMappingCategoricalColor
+ | DeprecatedColorMappingColorCode
+ | DeprecatedColorMappingLoopColor
+ >
+ >;
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/utils.test.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/utils.test.ts
new file mode 100644
index 0000000000000..342148f2e93bd
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/utils.test.ts
@@ -0,0 +1,153 @@
+/*
+ * 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 {
+ FormBasedLayer,
+ FormBasedPersistedState,
+ TextBasedPersistedState,
+} from '../../../../../../public';
+import type { TextBasedLayer } from '../../../../../../public/datasources/form_based/esql_layer/types';
+import type { StructuredDatasourceStates } from '../../../../../../public/react_embeddable/types';
+import { ColumnMeta, getColumnMetaFn } from './utils';
+
+const layerId = 'layer-1';
+const columnId = 'column-1';
+
+const getDatasourceStatesMock = (
+ type: keyof StructuredDatasourceStates,
+ dataType?: ColumnMeta['dataType'],
+ fieldType?: ColumnMeta['fieldType']
+) => {
+ if (type === 'formBased') {
+ return {
+ formBased: {
+ layers: {
+ [layerId]: {
+ columns: {
+ [columnId]: {
+ dataType,
+ params: {
+ parentFormat: { id: fieldType },
+ },
+ },
+ },
+ } as unknown as FormBasedLayer,
+ },
+ } satisfies FormBasedPersistedState,
+ };
+ }
+
+ if (type === 'textBased') {
+ return {
+ textBased: {
+ layers: {
+ [layerId]: {
+ columns: [{ columnId, meta: { type: dataType } }],
+ } as unknown as TextBasedLayer,
+ },
+ } satisfies TextBasedPersistedState,
+ };
+ }
+};
+
+describe('utils', () => {
+ describe('getColumnMetaFn', () => {
+ const mockDataType = 'string';
+ const mockFieldType = 'terms';
+
+ it('should return null if neither type exists', () => {
+ const mockDatasourceState = {};
+ const resultFn = getColumnMetaFn(mockDatasourceState);
+
+ expect(resultFn).toBeNull();
+ });
+
+ describe('formBased datasourceState', () => {
+ it('should correct dataType and fieldType', () => {
+ const mockDatasourceState = getDatasourceStatesMock(
+ 'formBased',
+ mockDataType,
+ mockFieldType
+ );
+ const resultFn = getColumnMetaFn(mockDatasourceState)!;
+ const result = resultFn(layerId, columnId);
+
+ expect(result.dataType).toBe(mockDataType);
+ expect(result.fieldType).toBe(mockFieldType);
+ });
+
+ it('should undefined dataType and fieldType if column not found', () => {
+ const mockDatasourceState = getDatasourceStatesMock('formBased');
+ const resultFn = getColumnMetaFn(mockDatasourceState)!;
+ const result = resultFn(layerId, 'bad-column');
+
+ expect(result.dataType).toBeUndefined();
+ expect(result.fieldType).toBeUndefined();
+ });
+
+ it('should undefined dataType and fieldType if layer not found', () => {
+ const mockDatasourceState = getDatasourceStatesMock('formBased');
+ const resultFn = getColumnMetaFn(mockDatasourceState)!;
+ const result = resultFn('bad-layer', columnId);
+
+ expect(result.dataType).toBeUndefined();
+ expect(result.fieldType).toBeUndefined();
+ });
+
+ it('should undefined dataType and fieldType if missing', () => {
+ const mockDatasourceState = getDatasourceStatesMock('formBased');
+ const resultFn = getColumnMetaFn(mockDatasourceState)!;
+ const result = resultFn(layerId, columnId);
+
+ expect(result.dataType).toBeUndefined();
+ expect(result.fieldType).toBeUndefined();
+ });
+ });
+
+ describe('textBased datasourceState', () => {
+ it('should correct dataType and fieldType', () => {
+ const mockDatasourceState = getDatasourceStatesMock(
+ 'textBased',
+ mockDataType,
+ mockFieldType
+ );
+ const resultFn = getColumnMetaFn(mockDatasourceState)!;
+ const result = resultFn(layerId, columnId);
+
+ expect(result.dataType).toBe(mockDataType);
+ expect(result.fieldType).toBeUndefined(); // no fieldType needed for textBased
+ });
+
+ it('should undefined dataType and fieldType if column not found', () => {
+ const mockDatasourceState = getDatasourceStatesMock('textBased');
+ const resultFn = getColumnMetaFn(mockDatasourceState)!;
+ const result = resultFn(layerId, 'bad-column');
+
+ expect(result.dataType).toBeUndefined();
+ expect(result.fieldType).toBeUndefined();
+ });
+
+ it('should undefined dataType and fieldType if layer not found', () => {
+ const mockDatasourceState = getDatasourceStatesMock('textBased');
+ const resultFn = getColumnMetaFn(mockDatasourceState)!;
+ const result = resultFn('bad-layer', columnId);
+
+ expect(result.dataType).toBeUndefined();
+ expect(result.fieldType).toBeUndefined();
+ });
+
+ it('should undefined dataType and fieldType if missing', () => {
+ const mockDatasourceState = getDatasourceStatesMock('textBased');
+ const resultFn = getColumnMetaFn(mockDatasourceState)!;
+ const result = resultFn(layerId, columnId);
+
+ expect(result.dataType).toBeUndefined();
+ expect(result.fieldType).toBeUndefined();
+ });
+ });
+ });
+});
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/utils.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/utils.ts
new file mode 100644
index 0000000000000..9a0cea6c89a77
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/common/utils.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { DatatableColumnType } from '@kbn/expressions-plugin/common';
+import type { GenericIndexPatternColumn } from '../../../../../../public';
+import type { StructuredDatasourceStates } from '../../../../../../public/react_embeddable/types';
+
+export interface ColumnMeta {
+ fieldType?: string | 'multi_terms' | 'range';
+ dataType?: GenericIndexPatternColumn['dataType'] | DatatableColumnType;
+}
+
+export function getColumnMetaFn(
+ datasourceStates?: StructuredDatasourceStates
+): ((layerId: string, columnId: string) => ColumnMeta) | null {
+ if (datasourceStates?.formBased?.layers) {
+ const layers = datasourceStates.formBased.layers;
+
+ return (layerId, columnId) => {
+ const column = layers[layerId]?.columns?.[columnId];
+ return {
+ fieldType:
+ column && 'params' in column
+ ? (column.params as { parentFormat?: { id?: string } })?.parentFormat?.id
+ : undefined,
+ dataType: column?.dataType,
+ };
+ };
+ }
+
+ if (datasourceStates?.textBased?.layers) {
+ const layers = datasourceStates.textBased.layers;
+
+ return (layerId, columnId) => {
+ const column = layers[layerId]?.columns?.find((c) => c.columnId === columnId);
+
+ return {
+ dataType: column?.meta?.type,
+ };
+ };
+ }
+
+ return null;
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/datatable.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/datatable.ts
new file mode 100644
index 0000000000000..5a440a15018e9
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/datatable.ts
@@ -0,0 +1,61 @@
+/*
+ * 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 { DatatableVisualizationState } from '../../../../../public';
+import type { GeneralDatasourceStates } from '../../../../../public/state_management';
+import { ColumnState } from '../../../../../common/expressions';
+import {
+ convertToRawColorMappings,
+ getColumnMetaFn,
+ isDeprecatedColorMapping,
+ type DeprecatedColorMappingConfig,
+} from './common';
+
+/** @deprecated */
+interface DeprecatedColorMappingColumn extends Omit {
+ colorMapping: DeprecatedColorMappingConfig;
+}
+
+/**
+ * Old color mapping state meant for type safety during runtime migrations of old configurations
+ *
+ * @deprecated Use respective vis state (i.e. `DatatableVisualizationState`)
+ */
+export interface DeprecatedColorMappingsDatatableState
+ extends Omit {
+ columns: Array;
+}
+
+export const convertDatatableToRawColorMappings = (
+ state: DatatableVisualizationState | DeprecatedColorMappingsDatatableState,
+ datasourceStates?: Readonly
+): DatatableVisualizationState => {
+ const getColumnMeta = getColumnMetaFn(datasourceStates);
+ const hasDeprecatedColorMappings = state.columns.some((column) => {
+ return isDeprecatedColorMapping(column.colorMapping);
+ });
+
+ if (!hasDeprecatedColorMappings) return state as DatatableVisualizationState;
+
+ const convertedColumns = state.columns.map((column) => {
+ if (column.colorMapping?.assignments || column.colorMapping?.specialAssignments) {
+ const columnMeta = getColumnMeta?.(state.layerId, column.columnId);
+
+ return {
+ ...column,
+ colorMapping: convertToRawColorMappings(column.colorMapping, columnMeta),
+ } satisfies ColumnState;
+ }
+
+ return column as ColumnState;
+ });
+
+ return {
+ ...state,
+ columns: convertedColumns,
+ } satisfies DatatableVisualizationState;
+};
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/index.test.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/index.test.ts
new file mode 100644
index 0000000000000..d3b72fb17f5f8
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/index.test.ts
@@ -0,0 +1,121 @@
+/*
+ * 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 { convertToRawColorMappingsFn } from '.';
+import { LensAttributes } from '../../types';
+import { convertXYToRawColorMappings } from './xy';
+import { convertPieToRawColorMappings } from './partition';
+import { convertDatatableToRawColorMappings } from './datatable';
+import { convertTagcloudToRawColorMappings } from './tagcloud';
+
+jest.mock('./xy', () => ({
+ convertXYToRawColorMappings: jest.fn().mockReturnValue('new xyVisState'),
+}));
+jest.mock('./partition', () => ({
+ convertPieToRawColorMappings: jest.fn().mockReturnValue('new partitionVisState'),
+}));
+jest.mock('./datatable', () => ({
+ convertDatatableToRawColorMappings: jest.fn().mockReturnValue('new datatableVisState'),
+}));
+jest.mock('./tagcloud', () => ({
+ convertTagcloudToRawColorMappings: jest.fn().mockReturnValue('new tagcloudVisState'),
+}));
+
+describe('Legend stat transforms', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return original attributes if no state', () => {
+ const attributes = {
+ state: undefined,
+ } as LensAttributes;
+ const result = convertToRawColorMappingsFn(attributes);
+
+ expect(result).toBe(attributes);
+ });
+
+ it('should return original attributes for noop visualizationTypes', () => {
+ const attributes = {
+ state: {},
+ visualizationType: 'noop',
+ } as LensAttributes;
+ const result = convertToRawColorMappingsFn(attributes);
+
+ expect(result).toBe(attributes);
+ });
+
+ it('should convert lnsXY attributes', () => {
+ const attributes = {
+ state: {
+ visualization: 'xyVisState',
+ datasourceStates: 'datasourceStates',
+ },
+ visualizationType: 'lnsXY',
+ } as LensAttributes;
+ const result = convertToRawColorMappingsFn(attributes);
+
+ expect(convertXYToRawColorMappings).toBeCalledWith('xyVisState', 'datasourceStates');
+ expect(result.state).toMatchObject({
+ visualization: 'new xyVisState',
+ });
+ });
+
+ it('should convert lnsPie attributes', () => {
+ const attributes = {
+ state: {
+ visualization: 'partitionVisState',
+ datasourceStates: 'datasourceStates',
+ },
+ visualizationType: 'lnsPie',
+ } as LensAttributes;
+ const result = convertToRawColorMappingsFn(attributes);
+
+ expect(convertPieToRawColorMappings).toBeCalledWith('partitionVisState', 'datasourceStates');
+ expect(result.state).toMatchObject({
+ visualization: 'new partitionVisState',
+ });
+ });
+
+ it('should convert lnsDatatable attributes', () => {
+ const attributes = {
+ state: {
+ visualization: 'datatableVisState',
+ datasourceStates: 'datasourceStates',
+ },
+ visualizationType: 'lnsDatatable',
+ } as LensAttributes;
+ const result = convertToRawColorMappingsFn(attributes);
+
+ expect(convertDatatableToRawColorMappings).toBeCalledWith(
+ 'datatableVisState',
+ 'datasourceStates'
+ );
+ expect(result.state).toMatchObject({
+ visualization: 'new datatableVisState',
+ });
+ });
+
+ it('should convert lnsTagcloud attributes', () => {
+ const attributes = {
+ state: {
+ visualization: 'tagcloudVisState',
+ datasourceStates: 'datasourceStates',
+ },
+ visualizationType: 'lnsTagcloud',
+ } as LensAttributes;
+ const result = convertToRawColorMappingsFn(attributes);
+
+ expect(convertTagcloudToRawColorMappings).toBeCalledWith(
+ 'tagcloudVisState',
+ 'datasourceStates'
+ );
+ expect(result.state).toMatchObject({
+ visualization: 'new tagcloudVisState',
+ });
+ });
+});
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/index.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/index.ts
new file mode 100644
index 0000000000000..6fea4e8bc4dad
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/index.ts
@@ -0,0 +1,87 @@
+/*
+ * 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 { DatatableVisualizationState, TagcloudState, XYState } from '../../../../../public';
+import type { StructuredDatasourceStates } from '../../../../../public/react_embeddable/types';
+import type { PieVisualizationState } from '../../../../../common/types';
+import type { LensAttributes } from '../../types';
+import { convertXYToRawColorMappings, type DeprecatedColorMappingsXYState } from './xy';
+import {
+ DeprecatedColorMappingPieVisualizationState,
+ convertPieToRawColorMappings,
+} from './partition';
+import {
+ convertDatatableToRawColorMappings,
+ type DeprecatedColorMappingsDatatableState,
+} from './datatable';
+import {
+ convertTagcloudToRawColorMappings,
+ type DeprecatedColorMappingTagcloudState,
+} from './tagcloud';
+
+export function convertToRawColorMappingsFn(attributes: LensAttributes): LensAttributes {
+ if (
+ !attributes.state ||
+ (attributes.visualizationType !== 'lnsXY' &&
+ attributes.visualizationType !== 'lnsPie' &&
+ attributes.visualizationType !== 'lnsDatatable' &&
+ attributes.visualizationType !== 'lnsTagcloud')
+ ) {
+ return attributes;
+ }
+
+ const newVisualizationState = getUpdatedVisualizationState(
+ attributes.visualizationType,
+ attributes.state as Record
+ );
+
+ return {
+ ...attributes,
+ state: {
+ ...(attributes.state as Record),
+ visualization: newVisualizationState,
+ },
+ };
+}
+
+export function getUpdatedVisualizationState(
+ visualizationType: LensAttributes['visualizationType'],
+ state: LensAttributes['state'] & {
+ visualization?: unknown;
+ datasourceStates?: unknown;
+ }
+): LensAttributes['state'] {
+ if (!state?.visualization) return state.visualization;
+
+ const datasourceStates = state.datasourceStates as StructuredDatasourceStates | undefined;
+
+ if (visualizationType === 'lnsXY') {
+ const visState = state.visualization as XYState | DeprecatedColorMappingsXYState;
+ return convertXYToRawColorMappings(visState, datasourceStates);
+ }
+
+ if (visualizationType === 'lnsPie') {
+ const visState = state.visualization as
+ | PieVisualizationState
+ | DeprecatedColorMappingPieVisualizationState;
+ return convertPieToRawColorMappings(visState, datasourceStates);
+ }
+
+ if (visualizationType === 'lnsDatatable') {
+ const visState = state.visualization as
+ | DatatableVisualizationState
+ | DeprecatedColorMappingsDatatableState;
+ return convertDatatableToRawColorMappings(visState, datasourceStates);
+ }
+
+ if (visualizationType === 'lnsTagcloud') {
+ const visState = state.visualization as TagcloudState | DeprecatedColorMappingTagcloudState;
+ return convertTagcloudToRawColorMappings(visState, datasourceStates);
+ }
+
+ return state.visualization;
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/partition.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/partition.ts
new file mode 100644
index 0000000000000..56247af84b654
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/partition.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { GeneralDatasourceStates } from '../../../../../public/state_management';
+import { PieLayerState, PieVisualizationState } from '../../../../../common/types';
+import {
+ convertToRawColorMappings,
+ getColumnMetaFn,
+ isDeprecatedColorMapping,
+ type DeprecatedColorMappingConfig,
+} from './common';
+
+/** @deprecated */
+interface DeprecatedColorMappingLayer extends Omit {
+ colorMapping: DeprecatedColorMappingConfig;
+}
+
+/**
+ * Old color mapping state meant for type safety during runtime migrations of old configurations
+ *
+ * @deprecated
+ */
+export interface DeprecatedColorMappingPieVisualizationState
+ extends Omit {
+ layers: Array;
+}
+
+export const convertPieToRawColorMappings = (
+ state: PieVisualizationState | DeprecatedColorMappingPieVisualizationState,
+ datasourceStates?: Readonly
+): PieVisualizationState => {
+ const getColumnMeta = getColumnMetaFn(datasourceStates);
+ const hasDeprecatedColorMappings = state.layers.some((layer) => {
+ return layer.layerType === 'data' && isDeprecatedColorMapping(layer.colorMapping);
+ });
+
+ if (!hasDeprecatedColorMappings) return state as PieVisualizationState;
+
+ const convertedLayers = state.layers.map((layer) => {
+ if (
+ layer.layerType === 'data' &&
+ (layer.colorMapping?.assignments || layer.colorMapping?.specialAssignments)
+ ) {
+ const [accessor] = layer.primaryGroups;
+ const columnMeta = accessor ? getColumnMeta?.(layer.layerId, accessor) : null;
+
+ return {
+ ...layer,
+ colorMapping: convertToRawColorMappings(layer.colorMapping, columnMeta),
+ } satisfies PieLayerState;
+ }
+
+ return layer as PieLayerState;
+ });
+
+ return {
+ ...state,
+ layers: convertedLayers,
+ } satisfies PieVisualizationState;
+};
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/tagcloud.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/tagcloud.ts
new file mode 100644
index 0000000000000..99b22927caba9
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/tagcloud.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 { TagcloudState } from '../../../../../public';
+import type { GeneralDatasourceStates } from '../../../../../public/state_management';
+import {
+ convertToRawColorMappings,
+ getColumnMetaFn,
+ isDeprecatedColorMapping,
+ type DeprecatedColorMappingConfig,
+} from './common';
+
+/**
+ * Old color mapping state meant for type safety during runtime migrations of old configurations
+ *
+ * @deprecated
+ */
+export interface DeprecatedColorMappingTagcloudState extends Omit {
+ colorMapping: DeprecatedColorMappingConfig;
+}
+
+export const convertTagcloudToRawColorMappings = (
+ state: TagcloudState | DeprecatedColorMappingTagcloudState,
+ datasourceStates?: Readonly
+): TagcloudState => {
+ const getColumnMeta = getColumnMetaFn(datasourceStates);
+ const hasDeprecatedColorMapping = state.colorMapping
+ ? isDeprecatedColorMapping(state.colorMapping)
+ : false;
+
+ if (!hasDeprecatedColorMapping) return state as TagcloudState;
+
+ const columnMeta = state.tagAccessor ? getColumnMeta?.(state.layerId, state.tagAccessor) : null;
+
+ return {
+ ...state,
+ colorMapping: state.colorMapping && convertToRawColorMappings(state.colorMapping, columnMeta),
+ } satisfies TagcloudState;
+};
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/types.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/types.ts
new file mode 100644
index 0000000000000..724cd5135c136
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/types.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { DeprecatedColorMappingsXYState } from './xy';
+import type { DeprecatedColorMappingPieVisualizationState } from './partition';
+import type { DeprecatedColorMappingsDatatableState } from './datatable';
+import type { DeprecatedColorMappingTagcloudState } from './tagcloud';
+
+export type DeprecatedColorMappingsState =
+ | DeprecatedColorMappingsXYState
+ | DeprecatedColorMappingPieVisualizationState
+ | DeprecatedColorMappingsDatatableState
+ | DeprecatedColorMappingTagcloudState;
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/xy.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/xy.ts
new file mode 100644
index 0000000000000..7930831db7e4e
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/raw_color_mappings/xy.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 { XYDataLayerConfig, XYLayerConfig, XYState } from '../../../../../public';
+import type { GeneralDatasourceStates } from '../../../../../public/state_management';
+import {
+ convertToRawColorMappings,
+ getColumnMetaFn,
+ isDeprecatedColorMapping,
+ type DeprecatedColorMappingConfig,
+} from './common';
+
+/** @deprecated */
+interface DeprecatedColorMappingLayer extends Omit {
+ colorMapping: DeprecatedColorMappingConfig;
+}
+
+/**
+ * Old color mapping state meant for type safety during runtime migrations of old configurations
+ *
+ * @deprecated
+ */
+export interface DeprecatedColorMappingsXYState extends Omit {
+ layers: Array;
+}
+
+export const convertXYToRawColorMappings = (
+ state: XYState | DeprecatedColorMappingsXYState,
+ datasourceStates?: Readonly
+): XYState => {
+ const getColumnMeta = getColumnMetaFn(datasourceStates);
+ const hasDeprecatedColorMappings = state.layers.some((layer) => {
+ return layer.layerType === 'data' && isDeprecatedColorMapping(layer.colorMapping);
+ });
+
+ if (!hasDeprecatedColorMappings) return state as XYState;
+
+ const convertedLayers = state.layers.map((layer) => {
+ if (
+ layer.layerType === 'data' &&
+ (layer.colorMapping?.assignments || layer.colorMapping?.specialAssignments)
+ ) {
+ const accessor = layer.splitAccessor;
+ const columnMeta = accessor ? getColumnMeta?.(layer.layerId, accessor) : null;
+
+ return {
+ ...layer,
+ colorMapping: convertToRawColorMappings(layer.colorMapping, columnMeta),
+ } satisfies XYDataLayerConfig;
+ }
+
+ return layer as XYLayerConfig;
+ });
+
+ return {
+ ...state,
+ layers: convertedLayers,
+ };
+};
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/transforms.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/transforms.ts
new file mode 100644
index 0000000000000..433d43b996d78
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/transforms.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { LensAttributes, LensSavedObject } from '../types';
+import { addVersion } from './add_version';
+import { convertToLegendStats } from './legend_stats';
+import { convertToRawColorMappingsFn } from './raw_color_mappings';
+import { LensSavedObjectV0, LensAttributesV0 } from './types';
+
+/**
+ * Transforms existing unversioned Lens SO attributes to v1 Lens Item attributes
+ *
+ * Includes:
+ * - Legend value → Legend stats
+ * - Stringified color mapping values → Raw color mappings values
+ * - Add version property
+ */
+export function transformToV1LensItemAttributes(
+ attributes: LensAttributesV0 | LensAttributes
+): LensAttributes {
+ return [convertToLegendStats, convertToRawColorMappingsFn, addVersion].reduce(
+ (newState, fn) => fn(newState),
+ attributes
+ );
+}
+
+/**
+ * Transforms existing unversioned Lens SO to v1 Lens SO
+ *
+ * Includes:
+ * - Legend value → Legend stats
+ * - Stringified color mapping values → Raw color mappings values
+ * - Add version property
+ */
+export function transformToV1LensSavedObject(
+ so: LensSavedObjectV0 | LensSavedObject
+): LensSavedObject {
+ return {
+ ...so,
+ attributes: transformToV1LensItemAttributes(so.attributes as LensAttributesV0 | LensAttributes),
+ };
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/types.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/types.ts
new file mode 100644
index 0000000000000..099c251459532
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/transforms/types.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { SOWithMetadata } from '@kbn/content-management-utils';
+
+import { LensAttributes } from '../types';
+import { DeprecatedLegendValueState } from './legend_stats/types';
+import { DeprecatedColorMappingsState } from './raw_color_mappings/types';
+
+export type DeprecatedV0State = DeprecatedLegendValueState | DeprecatedColorMappingsState;
+
+export type LensAttributesV0 = Omit & {
+ version: never; // explicitly set as no version
+ state: LensAttributes['state'] | DeprecatedV0State;
+};
+
+/**
+ * An unversioned Lens item that may or may not include old runtime migrations.
+ */
+export type LensSavedObjectV0 = SOWithMetadata;
diff --git a/x-pack/platform/plugins/shared/lens/server/content_management/v1/types.ts b/x-pack/platform/plugins/shared/lens/server/content_management/v1/types.ts
new file mode 100644
index 0000000000000..536f559b799ba
--- /dev/null
+++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/types.ts
@@ -0,0 +1,88 @@
+/*
+ * 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 { TypeOf } from '@kbn/config-schema';
+import type {
+ GetResultSO,
+ SOWithMetadata,
+ SOWithMetadataPartial,
+} from '@kbn/content-management-utils';
+import type {
+ CreateIn,
+ CreateResult,
+ DeleteIn,
+ DeleteResult,
+ GetIn,
+ SearchIn,
+ SearchResult,
+ UpdateIn,
+ UpdateResult,
+} from '@kbn/content-management-plugin/common';
+
+import {
+ lensItemAttributesSchema,
+ lensCMCreateOptionsSchema,
+ lensCMUpdateOptionsSchema,
+ lensCMSearchOptionsSchema,
+ lensItemSchema,
+ lensItemMetaSchema,
+ lensResponseItemSchema,
+ lensAPIAttributesSchema,
+ lensAPIConfigSchema,
+} from './schema';
+import { LENS_CONTENT_TYPE } from '../../../common/constants';
+
+export type LensAttributes = TypeOf;
+export type LensAPIAttributes = TypeOf;
+
+export type LensItem = TypeOf;
+export type LensItemMeta = TypeOf;
+
+export type LensAPIConfig = TypeOf;
+export type LensResponseItem = TypeOf;
+
+export type LensCreateOptions = TypeOf;
+export type LensUpdateOptions = TypeOf;
+export type LensSearchOptions = TypeOf;
+
+export type LensSavedObject = SOWithMetadata;
+export type LensPartialSavedObject = SOWithMetadataPartial;
+
+export type LensGetIn = GetIn;
+export type LensGetOut = GetResultSO;
+
+export type LensCreateIn = CreateIn;
+export type LensCreateOut = CreateResult;
+
+// Need to handle Lens UpdateIn a bit differently
+export type LensUpdateIn = UpdateIn;
+export type LensUpdateOut = UpdateResult;
+
+export type LensDeleteIn = DeleteIn;
+export type LensDeleteOut = DeleteResult;
+
+export type LensSearchIn = SearchIn;
+export type LensSearchOut = SearchResult;
+
+export interface LensCrud {
+ Attributes: LensAttributes;
+ Item: LensSavedObject;
+ PartialItem: LensPartialSavedObject;
+ GetIn: LensGetIn;
+ GetOut: LensGetOut;
+ CreateIn: LensCreateIn;
+ CreateOut: LensCreateOut;
+ CreateOptions: LensCreateOptions;
+ SearchIn: LensSearchIn;
+ SearchOut: LensSearchOut;
+ SearchOptions: LensSearchOptions;
+ UpdateIn: LensUpdateIn;
+ UpdateOut: LensUpdateOut;
+ UpdateOptions: LensUpdateOptions;
+ DeleteIn: LensDeleteIn;
+ DeleteOut: LensDeleteOut;
+}
diff --git a/x-pack/platform/plugins/shared/lens/server/index.ts b/x-pack/platform/plugins/shared/lens/server/index.ts
index d3f3eeb83c6c6..8f1bd4db15eb0 100644
--- a/x-pack/platform/plugins/shared/lens/server/index.ts
+++ b/x-pack/platform/plugins/shared/lens/server/index.ts
@@ -13,6 +13,32 @@ export const plugin = async (initContext: PluginInitializerContext) => {
return new LensServerPlugin(initContext);
};
-export { PUBLIC_API_PATH, PUBLIC_API_VERSION } from './api/constants';
+export {
+ lensGetRequestParamsSchema,
+ lensGetResponseBodySchema,
+ lensCreateRequestBodySchema,
+ lensCreateResponseBodySchema,
+ lensUpdateRequestParamsSchema,
+ lensUpdateRequestBodySchema,
+ lensUpdateResponseBodySchema,
+ lensDeleteRequestParamsSchema,
+ lensSearchRequestQuerySchema,
+ lensSearchResponseBodySchema,
+} from './api/schema';
export type { LensDocShape715 } from './migrations/types';
+
+export type {
+ LensCreateRequestBody,
+ LensCreateResponseBody,
+ LensUpdateRequestParams,
+ LensUpdateRequestBody,
+ LensUpdateResponseBody,
+ LensGetRequestParams,
+ LensGetResponseBody,
+ LensSearchRequestQuery,
+ LensSearchResponseBody,
+ LensDeleteRequestParams,
+ RegisterAPIRoutesArgs,
+ RegisterAPIRouteFn,
+} from './types';
diff --git a/x-pack/platform/plugins/shared/lens/server/plugin.tsx b/x-pack/platform/plugins/shared/lens/server/plugin.tsx
index b3c41328fc853..76a1fa1fba39f 100644
--- a/x-pack/platform/plugins/shared/lens/server/plugin.tsx
+++ b/x-pack/platform/plugins/shared/lens/server/plugin.tsx
@@ -28,9 +28,11 @@ import { setupExpressions } from './expressions';
import { makeLensEmbeddableFactory } from './embeddable/make_lens_embeddable_factory';
import type { CustomVisualizationMigrations } from './migrations/types';
import { LensAppLocatorDefinition } from '../common/locator/locator';
-import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
+import { LENS_CONTENT_TYPE, LENS_ITEM_LATEST_VERSION } from '../common/constants';
import { LensStorage } from './content_management';
import { registerLensAPIRoutes } from './api/routes';
+import { lensTransforms } from '../common/transforms/transforms';
+import { LENS_EMBEDDABLE_TYPE } from '../common/constants';
export interface PluginSetupContract {
taskManager?: TaskManagerSetupContract;
@@ -84,13 +86,13 @@ export class LensServerPlugin
}
plugins.contentManagement.register({
- id: CONTENT_ID,
+ id: LENS_CONTENT_TYPE,
storage: new LensStorage({
throwOnResultValidationError: this.initializerContext.env.mode.dev,
logger: this.initializerContext.logger.get('storage'),
}),
version: {
- latest: LATEST_VERSION,
+ latest: LENS_ITEM_LATEST_VERSION,
},
});
@@ -100,6 +102,7 @@ export class LensServerPlugin
this.customVisualizationMigrations
);
plugins.embeddable.registerEmbeddableFactory(lensEmbeddableFactory());
+ plugins.embeddable.registerTransforms(LENS_EMBEDDABLE_TYPE, lensTransforms);
registerLensAPIRoutes({
http: core.http,
diff --git a/x-pack/platform/plugins/shared/lens/server/saved_objects.ts b/x-pack/platform/plugins/shared/lens/server/saved_objects.ts
index 3b05772f7743c..02acda925f6a2 100644
--- a/x-pack/platform/plugins/shared/lens/server/saved_objects.ts
+++ b/x-pack/platform/plugins/shared/lens/server/saved_objects.ts
@@ -12,6 +12,11 @@ import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common';
import { getEditPath } from '../common/constants';
import { getAllMigrations } from './migrations/saved_object_migrations';
import { CustomVisualizationMigrations } from './migrations/types';
+import { lensItemAttributesSchemaV0 } from './content_management/v0';
+import {
+ LENS_ITEM_VERSION as LENS_ITEM_VERSION_V1,
+ lensItemAttributesSchema as lensItemAttributesSchemaV1,
+} from './content_management/v1';
export function setupSavedObjects(
core: CoreSetup,
@@ -40,6 +45,24 @@ export function setupSavedObjects(
DataViewPersistableStateService.getAllMigrations(),
customVisualizationMigrations
),
+ modelVersions: {
+ [LENS_ITEM_VERSION_V1]: {
+ changes: [
+ {
+ type: 'mappings_addition',
+ addedMappings: {
+ version: {
+ type: 'integer',
+ },
+ },
+ },
+ ],
+ schemas: {
+ forwardCompatibility: lensItemAttributesSchemaV1.extendsDeep({ unknowns: 'ignore' }),
+ create: lensItemAttributesSchemaV0,
+ },
+ },
+ },
mappings: {
properties: {
title: {
@@ -51,6 +74,9 @@ export function setupSavedObjects(
visualizationType: {
type: 'keyword',
},
+ version: {
+ type: 'integer',
+ },
state: {
dynamic: false,
properties: {},
diff --git a/x-pack/platform/plugins/shared/lens/common/content_management/types.ts b/x-pack/platform/plugins/shared/lens/server/types.ts
similarity index 86%
rename from x-pack/platform/plugins/shared/lens/common/content_management/types.ts
rename to x-pack/platform/plugins/shared/lens/server/types.ts
index 0bf6227d945e1..1966a3a2c5915 100644
--- a/x-pack/platform/plugins/shared/lens/common/content_management/types.ts
+++ b/x-pack/platform/plugins/shared/lens/server/types.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export type LensContentType = 'lens';
+export type * from './api/types';
diff --git a/x-pack/platform/plugins/shared/lens/tsconfig.json b/x-pack/platform/plugins/shared/lens/tsconfig.json
index f7e61c08abdf3..73814e63bd05b 100644
--- a/x-pack/platform/plugins/shared/lens/tsconfig.json
+++ b/x-pack/platform/plugins/shared/lens/tsconfig.json
@@ -125,6 +125,7 @@
"@kbn/deeplinks-analytics",
"@kbn/core-http-server",
"@kbn/presentation-util",
+ "@kbn/content-management-table-list-view-common",
],
"exclude": ["target/**/*"]
}
diff --git a/x-pack/platform/plugins/shared/maps/server/content_management/maps_storage.ts b/x-pack/platform/plugins/shared/maps/server/content_management/maps_storage.ts
index bd4f24255c461..d8dca9e134437 100644
--- a/x-pack/platform/plugins/shared/maps/server/content_management/maps_storage.ts
+++ b/x-pack/platform/plugins/shared/maps/server/content_management/maps_storage.ts
@@ -98,6 +98,7 @@ export class MapsStorage {
}
}
const { value, error: resultError } = transforms.get.out.result.down(
+ // @ts-expect-error - need to fix item type here
response,
undefined,
{ validate: false }
@@ -232,6 +233,7 @@ export class MapsStorage {
MapsUpdateOut,
MapsUpdateOut
>(
+ // @ts-expect-error - need to fix item type here
{ item },
undefined, // do not override version
{ validate: false } // validation is done above
diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/create/main.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/create/main.ts
index e390eb2f51bbe..d0cc483b4798a 100644
--- a/x-pack/platform/test/api_integration/apis/lens/visualizations/create/main.ts
+++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/create/main.ts
@@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
-import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
+import { LENS_VIS_API_PATH, LENS_API_VERSION } from '@kbn/lens-plugin/common/constants';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
@@ -18,9 +18,9 @@ export default function ({ getService }: FtrProviderContext) {
describe('main', () => {
it('should create a lens visualization', async () => {
const response = await supertest
- .post(`${PUBLIC_API_PATH}/visualizations`)
+ .post(LENS_VIS_API_PATH)
.set('kbn-xsrf', 'true')
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send(getExampleLensBody());
expect(response.status).to.be(201);
diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/create/validation.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/create/validation.ts
index 07992cbd74818..1d0338568ddc2 100644
--- a/x-pack/platform/test/api_integration/apis/lens/visualizations/create/validation.ts
+++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/create/validation.ts
@@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
-import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
+import { LENS_VIS_API_PATH, LENS_API_VERSION } from '@kbn/lens-plugin/common/constants';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
@@ -16,9 +16,9 @@ export default function ({ getService }: FtrProviderContext) {
describe('validation', () => {
it('should return error if body is empty', async () => {
const response = await supertest
- .post(`${PUBLIC_API_PATH}/visualizations`)
+ .post(LENS_VIS_API_PATH)
.set('kbn-xsrf', 'true')
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send({});
expect(response.status).to.be(400);
diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/delete/main.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/delete/main.ts
index b954b0e312363..261b95a299279 100644
--- a/x-pack/platform/test/api_integration/apis/lens/visualizations/delete/main.ts
+++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/delete/main.ts
@@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
-import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
+import { LENS_VIS_API_PATH, LENS_API_VERSION } from '@kbn/lens-plugin/common/constants';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
@@ -18,9 +18,9 @@ export default function ({ getService }: FtrProviderContext) {
it('should delete a lens visualization', async () => {
const id = '71c9c185-3e6d-49d0-b7e5-f966eaf51625'; // known id
const response = await supertest
- .delete(`${PUBLIC_API_PATH}/visualizations/${id}`)
+ .delete(`${LENS_VIS_API_PATH}/${id}`)
.set('kbn-xsrf', 'true')
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send();
expect(response.status).to.be(204);
@@ -29,9 +29,9 @@ export default function ({ getService }: FtrProviderContext) {
it('should error when deleting an unknown lens visualization', async () => {
const id = '123'; // unknown id
const response = await supertest
- .delete(`${PUBLIC_API_PATH}/visualizations/${id}`)
+ .delete(`${LENS_VIS_API_PATH}/${id}`)
.set('kbn-xsrf', 'true')
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send();
expect(response.status).to.be(404);
diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/get/main.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/get/main.ts
index f30684a456c87..e6ec2641618bb 100644
--- a/x-pack/platform/test/api_integration/apis/lens/visualizations/get/main.ts
+++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/get/main.ts
@@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
-import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
+import { LENS_VIS_API_PATH, LENS_API_VERSION } from '@kbn/lens-plugin/common/constants';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
@@ -18,8 +18,8 @@ export default function ({ getService }: FtrProviderContext) {
it('should get a lens visualization', async () => {
const id = '71c9c185-3e6d-49d0-b7e5-f966eaf51625'; // known id
const response = await supertest
- .get(`${PUBLIC_API_PATH}/visualizations/${id}`)
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .get(`${LENS_VIS_API_PATH}/${id}`)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send();
expect(response.status).to.be(200);
@@ -29,8 +29,8 @@ export default function ({ getService }: FtrProviderContext) {
it('should error when fetching an unknown lens visualization', async () => {
const id = '123'; // unknown id
const response = await supertest
- .get(`${PUBLIC_API_PATH}/visualizations/${id}`)
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .get(`${LENS_VIS_API_PATH}/${id}`)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send();
expect(response.status).to.be(404);
diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/search/main.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/search/main.ts
index f0a86f93ce854..c803574bc0749 100644
--- a/x-pack/platform/test/api_integration/apis/lens/visualizations/search/main.ts
+++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/search/main.ts
@@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
-import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
+import { LENS_VIS_API_PATH, LENS_API_VERSION } from '@kbn/lens-plugin/common/constants';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
@@ -17,8 +17,8 @@ export default function ({ getService }: FtrProviderContext) {
describe('main', () => {
it('should get list of lens visualizations', async () => {
const response = await supertest
- .get(`${PUBLIC_API_PATH}/visualizations`)
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .get(LENS_VIS_API_PATH)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send();
expect(response.status).to.be(200);
@@ -27,9 +27,9 @@ export default function ({ getService }: FtrProviderContext) {
it('should filter lens visualizations by title and description', async () => {
const response = await supertest
- .get(`${PUBLIC_API_PATH}/visualizations`)
+ .get(LENS_VIS_API_PATH)
.query({ query: '1' })
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send();
expect(response.status).to.be(200);
diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/search/validation.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/search/validation.ts
index 3cc8eeff85a00..65257c9e589ef 100644
--- a/x-pack/platform/test/api_integration/apis/lens/visualizations/search/validation.ts
+++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/search/validation.ts
@@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
-import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
+import { LENS_VIS_API_PATH, LENS_API_VERSION } from '@kbn/lens-plugin/common/constants';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
@@ -16,9 +16,9 @@ export default function ({ getService }: FtrProviderContext) {
describe('validation', () => {
it('should return error if using unknown params', async () => {
const response = await supertest
- .get(`${PUBLIC_API_PATH}/visualizations`)
+ .get(LENS_VIS_API_PATH)
.query({ xyz: 'unknown param' })
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send({});
expect(response.status).to.be(400);
diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/update/main.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/update/main.ts
index c5e38106a6ab1..8babe2b46bc60 100644
--- a/x-pack/platform/test/api_integration/apis/lens/visualizations/update/main.ts
+++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/update/main.ts
@@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
-import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
+import { LENS_VIS_API_PATH, LENS_API_VERSION } from '@kbn/lens-plugin/common/constants';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
@@ -19,9 +19,9 @@ export default function ({ getService }: FtrProviderContext) {
it('should update a lens visualization', async () => {
const id = '71c9c185-3e6d-49d0-b7e5-f966eaf51625'; // known id
const response = await supertest
- .put(`${PUBLIC_API_PATH}/visualizations/${id}`)
+ .put(`${LENS_VIS_API_PATH}/${id}`)
.set('kbn-xsrf', 'true')
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send(getExampleLensBody('Custom title'));
expect(response.status).to.be(200);
@@ -31,9 +31,9 @@ export default function ({ getService }: FtrProviderContext) {
it('should error when updating an unknown lens visualization', async () => {
const id = '123'; // unknown id
const response = await supertest
- .put(`${PUBLIC_API_PATH}/visualizations/${id}`)
+ .put(`${LENS_VIS_API_PATH}/${id}`)
.set('kbn-xsrf', 'true')
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send(getExampleLensBody('Custom title'));
expect(response.status).to.be(404);
diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/update/validation.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/update/validation.ts
index 777cc9cc82c2b..e1c4c0e269397 100644
--- a/x-pack/platform/test/api_integration/apis/lens/visualizations/update/validation.ts
+++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/update/validation.ts
@@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
-import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
+import { LENS_VIS_API_PATH, LENS_API_VERSION } from '@kbn/lens-plugin/common/constants';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
@@ -17,9 +17,9 @@ export default function ({ getService }: FtrProviderContext) {
it('should return error if body is empty', async () => {
const id = '71c9c185-3e6d-49d0-b7e5-f966eaf51625'; // known id
const response = await supertest
- .put(`${PUBLIC_API_PATH}/visualizations/${id}`)
+ .put(`${LENS_VIS_API_PATH}/${id}`)
.set('kbn-xsrf', 'true')
- .set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
+ .set(ELASTIC_HTTP_VERSION_HEADER, LENS_API_VERSION)
.send({});
expect(response.status).to.be(400);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/cost_savings_metric.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/cost_savings_metric.ts
index c42016efb33aa..bebe90e29321e 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/cost_savings_metric.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/cost_savings_metric.ts
@@ -118,8 +118,5 @@ export const getCostSavingsMetricLensAttributes: MyGetLensAttributes = ({
type: 'index-pattern',
},
],
- type: 'lens',
- updated_at: '2025-07-21T15:51:38.660Z',
- version: 'WzI0LDFd',
} as LensAttributes;
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/cost_savings_trend_area.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/cost_savings_trend_area.ts
index 84fe4144545dc..8e3c76eba5d95 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/cost_savings_trend_area.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/cost_savings_trend_area.ts
@@ -186,8 +186,5 @@ export const getCostSavingsTrendAreaLensAttributes: MyGetLensAttributes = ({
type: 'index-pattern',
},
],
- type: 'lens',
- updated_at: '2025-07-21T15:51:38.660Z',
- version: 'WzI0LDFd',
} as LensAttributes;
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/time_saved_metric.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/time_saved_metric.ts
index bbfcdd15b3583..eed364526637c 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/time_saved_metric.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/ai/time_saved_metric.ts
@@ -106,8 +106,5 @@ export const getTimeSavedMetricLensAttributes: MyGetLensAttributes = ({
type: 'index-pattern',
},
],
- type: 'lens',
- updated_at: '2025-07-21T15:51:38.660Z',
- version: 'WzI0LDFd',
} as LensAttributes;
};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_area.ts b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_area.ts
index 5120365243a4f..83911f6dd4597 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_area.ts
+++ b/x-pack/solutions/security/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_area.ts
@@ -128,8 +128,5 @@ export const getKpiUniqueIpsAreaLensAttributes: GetLensAttributes = ({ euiTheme
type: 'index-pattern',
},
],
- type: 'lens',
- updated_at: '2022-02-09T17:44:03.359Z',
- version: 'WzI5MTI5OSwzXQ==',
} as LensAttributes;
};