[Lens] Add internal CRUD api routes (#223296)

## Summary

This adds basic Lens CRUD api routes using the Content Management
system.

| Operation | URI |
|--------|--------|
| Create | `POST api/lens/visualizations` |
| Get | `GET api/lens/visualizations/{id}` |
| Search | `GET api/lens/visualizations?query=test` |
| Update | `PUT api/lens/visualizations/{id}` |
| Delete | `DELETE api/lens/visualizations/{id}` |

### Changes to Lens Content Management

The custom `update` method uses `soClient.create` under the hood for
reasons (i.e. #160116). However, doing this acts as an update or create
method with the provided `id`. I changed this behavior so now any update
where the id is not found will return a `404` error.

Closes #221941
Closes #221942 - OpenAPI docs auto generate from route schema

### Testing

You can testing this locally in kibana dev console like so...

```
GET kbn:/api/lens/visualizations/<id>?apiVersion=1
```

> The `apiVersion` query param is needed to test `internal` api routes.

## Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Marco Vettorello <marco.vettorello@elastic.co>
This commit is contained in:
Nick Partridge 2025-06-25 12:01:35 -07:00 committed by GitHub
parent 2c55a7d9d0
commit 17c2556fc6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 1582 additions and 37 deletions

View file

@ -378,6 +378,7 @@ enabled:
- x-pack/platform/test/api_integration/apis/management/config.ts
- x-pack/platform/test/api_integration/apis/management/index_management/disabled_data_enrichers/config.ts
- x-pack/platform/test/api_integration/apis/maps/config.ts
- x-pack/platform/test/api_integration/apis/lens/config.ts
- x-pack/platform/test/api_integration/apis/ml/config.ts
- x-pack/platform/test/api_integration/apis/monitoring/config.ts
- x-pack/platform/test/api_integration/apis/monitoring_collection/config.ts

4
.github/CODEOWNERS vendored
View file

@ -1262,7 +1262,8 @@ src/platform/plugins/shared/discover/public/context_awareness/profile_providers/
/x-pack/test/functional/page_objects/lens_page.ts @elastic/kibana-visualizations
/x-pack/test/functional/es_archives/lens @elastic/kibana-visualizations
/x-pack/test/examples/embedded_lens @elastic/kibana-visualizations
/x-pack/test/api_integration/fixtures/kbn_archiver/lens/constant_keyword.json @elastic/kibana-visualizations
/x-pack/test/api_integration/fixtures/kbn_archiver/lens/ @elastic/kibana-visualizations
/x-pack/platform/test/api_integration/apis/lens @elastic/kibana-visualizations
/src/platform/test/plugin_functional/test_suites/custom_visualizations @elastic/kibana-visualizations
/src/platform/test/plugin_functional/plugins/kbn_tp_custom_visualizations @elastic/kibana-visualizations
/x-pack/test/functional/fixtures/kbn_archiver/visualize @elastic/kibana-visualizations
@ -2742,7 +2743,6 @@ x-pack/solutions/security/plugins/security_solution/public/security_integrations
x-pack/solutions/security/plugins/security_solution/server/security_integrations @elastic/security-service-integrations
x-pack/solutions/security/plugins/security_solution/server/lib/security_integrations @elastic/security-service-integrations
# Kibana design
# scss overrides should be below this line for specificity
**/*.scss @elastic/kibana-design

View file

@ -56,25 +56,21 @@ 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 --------------
// ----------- UPDATE --------------
export type LensUpdateIn = LensCrudTypes['UpdateIn'];
export type LensUpdateOut = LensCrudTypes['UpdateOut'];
// ----------- DELETE --------------
// ----------- DELETE --------------
export type LensDeleteIn = LensCrudTypes['DeleteIn'];
export type LensDeleteOut = LensCrudTypes['DeleteOut'];
// ----------- SEARCH --------------
// ----------- SEARCH --------------
export type LensSearchIn = LensCrudTypes['SearchIn'];
export type LensSearchOut = LensCrudTypes['SearchOut'];

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 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';

View file

@ -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 { RegisterAPIRoutesArgs } from '../types';
import { registerLensVisualizationsAPIRoutes } from './visualizations';
export function registerLensAPIRoutes(args: RegisterAPIRoutesArgs) {
registerLensVisualizationsAPIRoutes(args);
}

View file

@ -0,0 +1,99 @@
/*
* 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 { 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 {
lensAttributesSchema,
lensCreateOptionsSchema,
lensSavedObjectSchema,
} from '../../../content_management/v1';
import { RegisterAPIRouteFn } from '../../types';
export const registerLensVisualizationsCreateAPIRoute: RegisterAPIRouteFn = (
router,
{ contentManagement }
) => {
const createRoute = router.post({
path: `${PUBLIC_API_PATH}/visualizations`,
access: PUBLIC_API_ACCESS,
enableQueryVersion: true,
summary: 'Create Lens visualization',
description: 'Create a new Lens visualization.',
options: {
tags: ['oas-tag:Lens'],
availability: {
stability: 'experimental',
},
},
security: {
authz: {
enabled: false,
reason: 'Relies on Content Client for authorization',
},
},
});
createRoute.addVersion(
{
version: PUBLIC_API_VERSION,
validate: {
request: {
body: schema.object({
options: lensCreateOptionsSchema,
data: lensAttributesSchema,
}),
},
response: {
201: {
body: () => lensSavedObjectSchema,
description: 'Created',
},
400: {
description: 'Malformed request',
},
401: {
description: 'Unauthorized',
},
403: {
description: 'Forbidden',
},
500: {
description: 'Internal Server Error',
},
},
},
},
async (ctx, req, res) => {
let result;
const { data, options } = req.body;
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
.for<LensSavedObject>(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
try {
({ result } = await client.create(data, options));
} catch (error) {
if (isBoom(error) && error.output.statusCode === 403) {
return res.forbidden();
}
return boomify(error); // forward unknown error
}
return res.created({ body: result.item });
}
);
};

View file

@ -0,0 +1,106 @@
/*
* 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 { 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 { RegisterAPIRouteFn } from '../../types';
export const registerLensVisualizationsDeleteAPIRoute: RegisterAPIRouteFn = (
router,
{ contentManagement }
) => {
const deleteRoute = router.delete({
path: `${PUBLIC_API_PATH}/visualizations/{id}`,
access: PUBLIC_API_ACCESS,
enableQueryVersion: true,
summary: 'Delete Lens visualization',
description: 'Delete a Lens visualization by id.',
options: {
tags: ['oas-tag:Lens'],
availability: {
stability: 'experimental',
},
},
security: {
authz: {
enabled: false,
reason: 'Relies on Content Client for authorization',
},
},
});
deleteRoute.addVersion(
{
version: PUBLIC_API_VERSION,
validate: {
request: {
params: schema.object({
id: schema.string({
meta: {
description: 'The saved object id of a Lens visualization.',
},
}),
}),
},
response: {
204: {
description: 'No Content',
},
400: {
description: 'Malformed request',
},
401: {
description: 'Unauthorized',
},
403: {
description: 'Forbidden',
},
404: {
description: 'Resource not found',
},
500: {
description: 'Internal Server Error',
},
},
},
},
async (ctx, req, res) => {
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
.for<LensSavedObject>(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
try {
await client.delete(req.params.id);
} 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.`,
},
});
}
if (error.output.statusCode === 403) {
return res.forbidden();
}
}
return boomify(error); // forward unknown error
}
return res.noContent();
}
);
};

View file

@ -0,0 +1,109 @@
/*
* 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 { 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 { lensSavedObjectSchema } from '../../../content_management/v1';
import { RegisterAPIRouteFn } from '../../types';
export const registerLensVisualizationsGetAPIRoute: RegisterAPIRouteFn = (
router,
{ contentManagement }
) => {
const getRoute = router.get({
path: `${PUBLIC_API_PATH}/visualizations/{id}`,
access: PUBLIC_API_ACCESS,
enableQueryVersion: true,
summary: 'Get Lens visualization',
description: 'Get a Lens visualization from id.',
options: {
tags: ['oas-tag:Lens'],
availability: {
stability: 'experimental',
},
},
security: {
authz: {
enabled: false,
reason: 'Relies on Content Client for authorization',
},
},
});
getRoute.addVersion(
{
version: PUBLIC_API_VERSION,
validate: {
request: {
params: schema.object({
id: schema.string({
meta: {
description: 'The saved object id of a Lens visualization.',
},
}),
}),
},
response: {
200: {
body: () => lensSavedObjectSchema,
description: 'Ok',
},
400: {
description: 'Malformed request',
},
401: {
description: 'Unauthorized',
},
403: {
description: 'Forbidden',
},
404: {
description: 'Resource not found',
},
500: {
description: 'Internal Server Error',
},
},
},
},
async (ctx, req, res) => {
let result;
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
.for<LensSavedObject>(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
try {
({ result } = await client.get(req.params.id));
} 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.`,
},
});
}
if (error.output.statusCode === 403) {
return res.forbidden();
}
}
return boomify(error); // forward unknown error
}
return res.ok({ body: result.item });
}
);
};

View file

@ -0,0 +1,23 @@
/*
* 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 { RegisterAPIRoutesArgs } from '../../types';
import { registerLensVisualizationsCreateAPIRoute } from './create';
import { registerLensVisualizationsGetAPIRoute } from './get';
import { registerLensVisualizationsUpdateAPIRoute } from './update';
import { registerLensVisualizationsDeleteAPIRoute } from './delete';
import { registerLensVisualizationsSearchAPIRoute } from './search';
export function registerLensVisualizationsAPIRoutes({ http, ...rest }: RegisterAPIRoutesArgs) {
const { versioned: versionedRouter } = http.createRouter();
registerLensVisualizationsCreateAPIRoute(versionedRouter, rest);
registerLensVisualizationsGetAPIRoute(versionedRouter, rest);
registerLensVisualizationsUpdateAPIRoute(versionedRouter, rest);
registerLensVisualizationsDeleteAPIRoute(versionedRouter, rest);
registerLensVisualizationsSearchAPIRoute(versionedRouter, rest);
}

View file

@ -0,0 +1,124 @@
/*
* 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 { isBoom, boomify } 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 { lensSavedObjectSchema } from '../../../content_management/v1';
import { RegisterAPIRouteFn } from '../../types';
export const registerLensVisualizationsSearchAPIRoute: RegisterAPIRouteFn = (
router,
{ contentManagement }
) => {
const searchRoute = router.get({
path: `${PUBLIC_API_PATH}/visualizations`,
access: PUBLIC_API_ACCESS,
enableQueryVersion: true,
summary: 'Search Lens visualizations',
description: 'Get list of Lens visualizations.',
options: {
tags: ['oas-tag:Lens'],
availability: {
stability: 'experimental',
},
},
security: {
authz: {
enabled: false,
reason: 'Relies on Content Client for authorization',
},
},
});
searchRoute.addVersion(
{
version: PUBLIC_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,
}),
}),
},
response: {
200: {
body: () => schema.arrayOf(lensSavedObjectSchema),
description: 'Ok',
},
400: {
description: 'Malformed request',
},
401: {
description: 'Unauthorized',
},
403: {
description: 'Forbidden',
},
500: {
description: 'Internal Server Error',
},
},
},
},
async (ctx, req, res) => {
let result;
const { query, page, perPage: limit } = req.query;
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
.for<LensSavedObject>(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
try {
({ result } = await client.search(
{
text: query,
cursor: page.toString(),
limit,
},
{
searchFields: ['title', 'description'],
}
));
} catch (error) {
if (isBoom(error) && error.output.statusCode === 403) {
return res.forbidden();
}
return boomify(error); // forward unknown error
}
return res.ok({ body: result.hits });
}
);
};

View file

@ -0,0 +1,118 @@
/*
* 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 { 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 {
lensAttributesSchema,
lensCreateOptionsSchema,
lensSavedObjectSchema,
} from '../../../content_management/v1';
import { RegisterAPIRouteFn } from '../../types';
export const registerLensVisualizationsUpdateAPIRoute: RegisterAPIRouteFn = (
router,
{ contentManagement }
) => {
const updateRoute = router.put({
path: `${PUBLIC_API_PATH}/visualizations/{id}`,
access: PUBLIC_API_ACCESS,
enableQueryVersion: true,
summary: 'Update Lens visualization',
description: 'Update an existing Lens visualization.',
options: {
tags: ['oas-tag:Lens'],
availability: {
stability: 'experimental',
},
},
security: {
authz: {
enabled: false,
reason: 'Relies on Content Client for authorization',
},
},
});
updateRoute.addVersion(
{
version: PUBLIC_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,
}),
},
response: {
200: {
body: () => lensSavedObjectSchema,
description: 'Ok',
},
400: {
description: 'Malformed request',
},
401: {
description: 'Unauthorized',
},
403: {
description: 'Forbidden',
},
404: {
description: 'Resource not found',
},
500: {
description: 'Internal Server Error',
},
},
},
},
async (ctx, req, res) => {
let result;
const { data, options } = req.body;
const client = contentManagement.contentClient
.getForRequest({ request: req, requestHandlerContext: ctx })
.for<LensSavedObject>(CONTENT_ID, PUBLIC_API_CONTENT_MANAGEMENT_VERSION);
try {
({ result } = await client.update(req.params.id, data, options));
} 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.`,
},
});
}
if (error.output.statusCode === 403) {
return res.forbidden();
}
}
return boomify(error); // forward unknown error
}
return res.ok({ body: result.item });
}
);
};

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { HttpServiceSetup, Logger, RequestHandlerContext } from '@kbn/core/server';
import { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
import { VersionedRouter } from '@kbn/core-http-server';
export interface RegisterAPIRoutesArgs {
http: HttpServiceSetup;
contentManagement: ContentManagementServerSetup;
logger: Logger;
}
export type RegisterAPIRouteFn = (
router: VersionedRouter<RequestHandlerContext>,
args: Omit<RegisterAPIRoutesArgs, 'http'>
) => void;

View file

@ -136,6 +136,9 @@ export class LensStorage extends SOContentStorage<LensCrudTypes> {
// Save data in DB
const soClient = await savedObjectClientFromRequest(ctx);
// since we use create below this is to throw if SO id not found
await soClient.get(CONTENT_ID, id);
const savedObject = await soClient.create<LensSavedObjectAttributes>(CONTENT_ID, dataToLatest, {
id,
overwrite: true,

View file

@ -25,7 +25,7 @@ const referenceSchema = schema.object(
const referencesSchema = schema.arrayOf(referenceSchema);
const lensAttributesSchema = schema.object(
export const lensAttributesSchema = schema.object(
{
title: schema.string(),
description: schema.maybe(schema.nullable(schema.string())),
@ -38,7 +38,7 @@ const lensAttributesSchema = schema.object(
{ unknowns: 'forbid' }
);
const lensSavedObjectSchema = schema.object(
export const lensSavedObjectSchema = schema.object(
{
id: schema.string(),
type: schema.string(),
@ -54,7 +54,7 @@ const lensSavedObjectSchema = schema.object(
{ unknowns: 'allow' }
);
const getResultSchema = schema.object(
const lensGetResultSchema = schema.object(
{
item: lensSavedObjectSchema,
meta: schema.object(
@ -78,63 +78,72 @@ const getResultSchema = schema.object(
{ unknowns: 'forbid' }
);
const createOptionsSchema = schema.object({
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: getResultSchema,
schema: lensGetResultSchema,
},
},
},
create: {
in: {
options: {
schema: createOptionsSchema,
},
data: {
schema: lensAttributesSchema,
},
options: {
schema: lensCreateOptionsSchema,
},
},
out: {
result: {
schema: schema.object(
{
item: lensSavedObjectSchema,
},
{ unknowns: 'forbid' }
),
schema: lensCreateResultSchema,
},
},
},
update: {
in: {
options: {
schema: createOptionsSchema, // same schema as "create"
},
data: {
schema: lensAttributesSchema,
},
options: {
schema: lensCreateOptionsSchema,
},
},
out: {
result: {
schema: lensCreateResultSchema,
},
},
},
search: {
in: {
options: {
schema: schema.maybe(
schema.object(
{
searchFields: schema.maybe(schema.arrayOf(schema.string())),
types: schema.maybe(schema.arrayOf(schema.string())),
},
{ unknowns: 'forbid' }
)
),
schema: lensSearchOptionsSchema,
},
},
},

View file

@ -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 './cm_services';

View file

@ -13,4 +13,6 @@ export const plugin = async (initContext: PluginInitializerContext) => {
return new LensServerPlugin(initContext);
};
export { PUBLIC_API_PATH, PUBLIC_API_VERSION } from './api/constants';
export type { LensDocShape715 } from './migrations/types';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from '@kbn/core/server';
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext, Logger } from '@kbn/core/server';
import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import {
PluginStart as DataPluginStart,
@ -30,6 +30,7 @@ import type { CustomVisualizationMigrations } from './migrations/types';
import { LensAppLocatorDefinition } from '../common/locator/locator';
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
import { LensStorage } from './content_management';
import { registerLensAPIRoutes } from './api/routes';
export interface PluginSetupContract {
taskManager?: TaskManagerSetupContract;
@ -65,8 +66,11 @@ export class LensServerPlugin
implements Plugin<LensServerPluginSetup, {}, PluginSetupContract, PluginStartContract>
{
private customVisualizationMigrations: CustomVisualizationMigrations = {};
private readonly logger: Logger;
constructor(private initializerContext: PluginInitializerContext) {}
constructor(private initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
setup(core: CoreSetup<PluginStartContract>, plugins: PluginSetupContract) {
const getFilterMigrations = plugins.data.query.filterManager.getAllMigrations.bind(
@ -96,6 +100,13 @@ export class LensServerPlugin
this.customVisualizationMigrations
);
plugins.embeddable.registerEmbeddableFactory(lensEmbeddableFactory());
registerLensAPIRoutes({
http: core.http,
contentManagement: plugins.contentManagement,
logger: this.logger,
});
return {
lensEmbeddableFactory,
registerVisualizationMigration: (

View file

@ -124,6 +124,7 @@
"@kbn/fields-metadata-plugin",
"@kbn/alerts-ui-shared",
"@kbn/deeplinks-analytics",
"@kbn/core-http-server",
],
"exclude": ["target/**/*"]
}

View file

@ -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 { FtrConfigProviderContext } from '@kbn/test';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts'));
return {
...baseIntegrationTestsConfig.getAll(),
testFiles: [require.resolve('.')],
};
}

View file

@ -0,0 +1,74 @@
/*
* 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 const getExampleLensBody = (title = `Lens vis - ${Date.now()} - ${Math.random()}`) => ({
data: {
title,
description: '',
visualizationType: 'lnsMetric',
state: {
visualization: {
layerId: '32e889c6-89f9-4873-b1f7-d5bea381c582',
layerType: 'data',
metricAccessor: '1c6729bc-ec92-4000-8dcc-0fdd7b56d5b8',
secondaryTrend: {
type: 'none',
},
},
query: {
query: '',
language: 'kuery',
},
filters: [],
datasourceStates: {
formBased: {
layers: {
'32e889c6-89f9-4873-b1f7-d5bea381c582': {
columns: {
'1c6729bc-ec92-4000-8dcc-0fdd7b56d5b8': {
label: 'Count of records',
dataType: 'number',
operationType: 'count',
isBucketed: false,
scale: 'ratio',
sourceField: '___records___',
params: {
emptyAsNull: true,
},
},
},
columnOrder: ['1c6729bc-ec92-4000-8dcc-0fdd7b56d5b8'],
incompleteColumns: {
'd0b92889-f74c-4194-b738-76eb5d268524': {
operationType: 'date_histogram',
},
},
sampling: 1,
},
},
},
indexpattern: {
layers: {},
},
textBased: {
layers: {},
},
},
internalReferences: [],
adHocDataViews: {},
},
},
options: {
references: [
{
type: 'index-pattern',
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
name: 'indexpattern-datasource-layer-32e889c6-89f9-4873-b1f7-d5bea381c582',
},
],
},
});

View file

@ -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 { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('lens', () => {
loadTestFile(require.resolve('./visualizations'));
});
}

View file

@ -0,0 +1,28 @@
/*
* 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 { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
describe('visualizations - create', () => {
before(async () => {
await kibanaServer.importExport.load(
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
});
after(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.unload(
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
});
loadTestFile(require.resolve('./main'));
loadTestFile(require.resolve('./validation'));
});
}

View file

@ -0,0 +1,29 @@
/*
* 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 expect from '@kbn/expect';
import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
import { getExampleLensBody } from '../../examples';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('main', () => {
it('should create a lens visualization', async () => {
const response = await supertest
.post(`${PUBLIC_API_PATH}/visualizations`)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
.send(getExampleLensBody());
expect(response.status).to.be(201);
});
});
}

View file

@ -0,0 +1,30 @@
/*
* 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 expect from '@kbn/expect';
import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('validation', () => {
it('should return error if body is empty', async () => {
const response = await supertest
.post(`${PUBLIC_API_PATH}/visualizations`)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
.send({});
expect(response.status).to.be(400);
expect(response.body.message).to.be(
'[request body.data.title]: expected value of type [string] but got [undefined]'
);
});
});
}

View file

@ -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 type { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
describe('visualizations - create', () => {
before(async () => {
await kibanaServer.importExport.load(
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.load(
'x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json'
);
});
after(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.unload(
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.unload(
'x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json'
);
});
loadTestFile(require.resolve('./main'));
});
}

View file

@ -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 expect from '@kbn/expect';
import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('main', () => {
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}`)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
.send();
expect(response.status).to.be(204);
});
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}`)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
.send();
expect(response.status).to.be(404);
expect(response.body.message).to.be(
'A Lens visualization with saved object id [123] was not found.'
);
});
});
}

View file

@ -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 type { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
describe('visualizations - create', () => {
before(async () => {
await kibanaServer.importExport.load(
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.load(
'x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json'
);
});
after(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.unload(
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.unload(
'x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json'
);
});
loadTestFile(require.resolve('./main'));
});
}

View file

@ -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 expect from '@kbn/expect';
import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('main', () => {
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)
.send();
expect(response.status).to.be(200);
expect(response.body.attributes.title).to.be('Lens example - 1');
});
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)
.send();
expect(response.status).to.be(404);
expect(response.body.message).to.be(
'A Lens visualization with saved object id [123] was not found.'
);
});
});
}

View file

@ -0,0 +1,18 @@
/*
* 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 { FtrProviderContext } from '../../../../functional/ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('visualizations', () => {
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./update'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./search'));
});
}

View file

@ -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 { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
describe('visualizations - update', () => {
before(async () => {
await kibanaServer.importExport.load(
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.load(
'x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json'
);
});
after(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.unload(
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.unload(
'x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json'
);
});
loadTestFile(require.resolve('./main'));
loadTestFile(require.resolve('./validation'));
});
}

View file

@ -0,0 +1,39 @@
/*
* 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 expect from '@kbn/expect';
import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
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)
.send();
expect(response.status).to.be(200);
expect(response.body.length).to.be(4);
});
it('should filter lens visualizations by title and description', async () => {
const response = await supertest
.get(`${PUBLIC_API_PATH}/visualizations`)
.query({ query: '1' })
.set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
.send();
expect(response.status).to.be(200);
expect(response.body.length).to.be(2);
});
});
}

View file

@ -0,0 +1,30 @@
/*
* 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 expect from '@kbn/expect';
import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('validation', () => {
it('should return error if using unknown params', async () => {
const response = await supertest
.get(`${PUBLIC_API_PATH}/visualizations`)
.query({ xyz: 'unknown param' })
.set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
.send({});
expect(response.status).to.be(400);
expect(response.body.message).to.be(
'[request query.xyz]: definition for this key is missing'
);
});
});
}

View file

@ -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 { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
describe('visualizations - update', () => {
before(async () => {
await kibanaServer.importExport.load(
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.load(
'x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json'
);
});
after(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.unload(
'src/platform/test/api_integration/fixtures/kbn_archiver/saved_objects/basic.json'
);
await kibanaServer.importExport.unload(
'x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json'
);
});
loadTestFile(require.resolve('./main'));
loadTestFile(require.resolve('./validation'));
});
}

View file

@ -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 expect from '@kbn/expect';
import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
import { getExampleLensBody } from '../../examples';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('main', () => {
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}`)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
.send(getExampleLensBody('Custom title'));
expect(response.status).to.be(200);
expect(response.body.attributes.title).to.be('Custom title');
});
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}`)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
.send(getExampleLensBody('Custom title'));
expect(response.status).to.be(404);
expect(response.body.message).to.be(
'A Lens visualization with saved object id [123] was not found.'
);
});
});
}

View file

@ -0,0 +1,31 @@
/*
* 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 expect from '@kbn/expect';
import { PUBLIC_API_PATH, PUBLIC_API_VERSION } from '@kbn/lens-plugin/server';
import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import type { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('validation', () => {
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}`)
.set('kbn-xsrf', 'true')
.set(ELASTIC_HTTP_VERSION_HEADER, PUBLIC_API_VERSION)
.send({});
expect(response.status).to.be(400);
expect(response.body.message).to.be(
'[request body.data.title]: expected value of type [string] but got [undefined]'
);
});
});
}

View file

@ -105,5 +105,6 @@
"@kbn/repo-info",
"@kbn/dev-cli-errors",
"@kbn/journeys",
"@kbn/lens-plugin",
]
}

View file

@ -0,0 +1,315 @@
{
"id": "71c9c185-3e6d-49d0-b7e5-f966eaf51625",
"type": "lens",
"namespaces": [
"default"
],
"updated_at": "2025-06-17T15:47:13.313Z",
"created_at": "2025-06-17T15:47:13.313Z",
"version": "WzU5LDFd",
"attributes": {
"title": "Lens example - 1",
"description": "",
"visualizationType": "lnsMetric",
"state": {
"visualization": {
"layerId": "7aa8fd7f-f664-48fe-8232-3a26054f9cdc",
"layerType": "data",
"metricAccessor": "89a69d8d-a6bc-47a8-80c6-94272762e785",
"secondaryTrend": {
"type": "none"
}
},
"query": {
"query": "",
"language": "kuery"
},
"filters": [],
"datasourceStates": {
"formBased": {
"layers": {
"7aa8fd7f-f664-48fe-8232-3a26054f9cdc": {
"columns": {
"89a69d8d-a6bc-47a8-80c6-94272762e785": {
"label": "Count of records",
"dataType": "number",
"operationType": "count",
"isBucketed": false,
"scale": "ratio",
"sourceField": "___records___",
"params": {
"emptyAsNull": true
}
}
},
"columnOrder": [
"89a69d8d-a6bc-47a8-80c6-94272762e785"
],
"incompleteColumns": {
"806819b9-b606-4383-9337-e6a40b8602ad": {
"operationType": "date_histogram"
}
},
"sampling": 1
}
}
},
"indexpattern": {
"layers": {}
},
"textBased": {
"layers": {}
}
},
"internalReferences": [],
"adHocDataViews": {}
}
},
"references": [
{
"type": "index-pattern",
"id": "91200a00-9efd-11e7-acb3-3dab96693fab",
"name": "indexpattern-datasource-layer-7aa8fd7f-f664-48fe-8232-3a26054f9cdc"
}
],
"managed": false,
"coreMigrationVersion": "8.8.0",
"typeMigrationVersion": "8.9.0"
}
{
"id": "4fad4960-5be9-408c-854b-3b53ac80df81",
"type": "lens",
"namespaces": [
"default"
],
"updated_at": "2025-06-17T15:48:59.917Z",
"created_at": "2025-06-17T15:48:59.917Z",
"version": "WzYyLDFd",
"attributes": {
"title": "Lens example - 2",
"description": "",
"visualizationType": "lnsMetric",
"state": {
"visualization": {
"layerId": "7aa8fd7f-f664-48fe-8232-3a26054f9cdc",
"layerType": "data",
"metricAccessor": "89a69d8d-a6bc-47a8-80c6-94272762e785",
"secondaryTrend": {
"type": "none"
}
},
"query": {
"query": "",
"language": "kuery"
},
"filters": [],
"datasourceStates": {
"formBased": {
"layers": {
"7aa8fd7f-f664-48fe-8232-3a26054f9cdc": {
"columns": {
"89a69d8d-a6bc-47a8-80c6-94272762e785": {
"label": "Count of records",
"dataType": "number",
"operationType": "count",
"isBucketed": false,
"scale": "ratio",
"sourceField": "___records___",
"params": {
"emptyAsNull": true
}
}
},
"columnOrder": [
"89a69d8d-a6bc-47a8-80c6-94272762e785"
],
"incompleteColumns": {
"806819b9-b606-4383-9337-e6a40b8602ad": {
"operationType": "date_histogram"
}
},
"sampling": 1
}
}
},
"indexpattern": {
"layers": {}
},
"textBased": {
"layers": {}
}
},
"internalReferences": [],
"adHocDataViews": {}
}
},
"references": [
{
"type": "index-pattern",
"id": "91200a00-9efd-11e7-acb3-3dab96693fab",
"name": "indexpattern-datasource-layer-7aa8fd7f-f664-48fe-8232-3a26054f9cdc"
}
],
"managed": false,
"coreMigrationVersion": "8.8.0",
"typeMigrationVersion": "8.9.0"
}
{
"id": "af239293-933a-4f35-969f-9dcccfefa8e4",
"type": "lens",
"namespaces": [
"default"
],
"updated_at": "2025-06-17T15:50:00.097Z",
"created_at": "2025-06-17T15:50:00.097Z",
"version": "WzY0LDFd",
"attributes": {
"title": "Lens example - 3",
"description": "Some description - 1",
"visualizationType": "lnsMetric",
"state": {
"visualization": {
"layerId": "7aa8fd7f-f664-48fe-8232-3a26054f9cdc",
"layerType": "data",
"metricAccessor": "89a69d8d-a6bc-47a8-80c6-94272762e785",
"secondaryTrend": {
"type": "none"
}
},
"query": {
"query": "",
"language": "kuery"
},
"filters": [],
"datasourceStates": {
"formBased": {
"layers": {
"7aa8fd7f-f664-48fe-8232-3a26054f9cdc": {
"columns": {
"89a69d8d-a6bc-47a8-80c6-94272762e785": {
"label": "Count of records",
"dataType": "number",
"operationType": "count",
"isBucketed": false,
"scale": "ratio",
"sourceField": "___records___",
"params": {
"emptyAsNull": true
}
}
},
"columnOrder": [
"89a69d8d-a6bc-47a8-80c6-94272762e785"
],
"incompleteColumns": {
"806819b9-b606-4383-9337-e6a40b8602ad": {
"operationType": "date_histogram"
}
},
"sampling": 1
}
}
},
"indexpattern": {
"layers": {}
},
"textBased": {
"layers": {}
}
},
"internalReferences": [],
"adHocDataViews": {}
}
},
"references": [
{
"type": "index-pattern",
"id": "91200a00-9efd-11e7-acb3-3dab96693fab",
"name": "indexpattern-datasource-layer-7aa8fd7f-f664-48fe-8232-3a26054f9cdc"
}
],
"managed": false,
"coreMigrationVersion": "8.8.0",
"typeMigrationVersion": "8.9.0"
}
{
"id": "f708d303-2418-4313-aa28-c2830f7cf4cd",
"type": "lens",
"namespaces": [
"default"
],
"updated_at": "2025-06-17T15:50:05.680Z",
"created_at": "2025-06-17T15:50:05.680Z",
"version": "WzY2LDFd",
"attributes": {
"title": "Lens example - 4",
"description": "",
"visualizationType": "lnsMetric",
"state": {
"visualization": {
"layerId": "7aa8fd7f-f664-48fe-8232-3a26054f9cdc",
"layerType": "data",
"metricAccessor": "89a69d8d-a6bc-47a8-80c6-94272762e785",
"secondaryTrend": {
"type": "none"
}
},
"query": {
"query": "",
"language": "kuery"
},
"filters": [],
"datasourceStates": {
"formBased": {
"layers": {
"7aa8fd7f-f664-48fe-8232-3a26054f9cdc": {
"columns": {
"89a69d8d-a6bc-47a8-80c6-94272762e785": {
"label": "Count of records",
"dataType": "number",
"operationType": "count",
"isBucketed": false,
"scale": "ratio",
"sourceField": "___records___",
"params": {
"emptyAsNull": true
}
}
},
"columnOrder": [
"89a69d8d-a6bc-47a8-80c6-94272762e785"
],
"incompleteColumns": {
"806819b9-b606-4383-9337-e6a40b8602ad": {
"operationType": "date_histogram"
}
},
"sampling": 1
}
}
},
"indexpattern": {
"layers": {}
},
"textBased": {
"layers": {}
}
},
"internalReferences": [],
"adHocDataViews": {}
}
},
"references": [
{
"type": "index-pattern",
"id": "91200a00-9efd-11e7-acb3-3dab96693fab",
"name": "indexpattern-datasource-layer-7aa8fd7f-f664-48fe-8232-3a26054f9cdc"
}
],
"managed": false,
"coreMigrationVersion": "8.8.0",
"typeMigrationVersion": "8.9.0"
}