diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index 63c29e80d926..7a1aadf78453 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -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 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c12fcb9cba29..81b8d0d5e0bc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 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 index d722936cdbfa..57264e3a4c5b 100644 --- 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 @@ -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']; diff --git a/x-pack/platform/plugins/shared/lens/server/api/constants.ts b/x-pack/platform/plugins/shared/lens/server/api/constants.ts new file mode 100644 index 000000000000..ef40c7044882 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/api/constants.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; 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'; 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 new file mode 100644 index 000000000000..9515f4da9df0 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/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. + */ + +import { RegisterAPIRoutesArgs } from '../types'; +import { registerLensVisualizationsAPIRoutes } from './visualizations'; + +export function registerLensAPIRoutes(args: RegisterAPIRoutesArgs) { + registerLensVisualizationsAPIRoutes(args); +} 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 new file mode 100644 index 000000000000..e1fc00bf06ba --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/create.ts @@ -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(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 }); + } + ); +}; 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 new file mode 100644 index 000000000000..b8a596fac025 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/delete.ts @@ -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(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(); + } + ); +}; 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 new file mode 100644 index 000000000000..185a897bf398 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/get.ts @@ -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(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 }); + } + ); +}; diff --git a/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/index.ts b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/index.ts new file mode 100644 index 000000000000..dd820226e407 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/index.ts @@ -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); +} 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 new file mode 100644 index 000000000000..3ab47a50824c --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/search.ts @@ -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(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 }); + } + ); +}; 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 new file mode 100644 index 000000000000..1f3279a5976d --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/api/routes/visualizations/update.ts @@ -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(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 }); + } + ); +}; diff --git a/x-pack/platform/plugins/shared/lens/server/api/types.ts b/x-pack/platform/plugins/shared/lens/server/api/types.ts new file mode 100644 index 000000000000..68ff2865183b --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/api/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; 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, + args: Omit +) => void; 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 e17ea543b8bd..41a0b0953a77 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 @@ -136,6 +136,9 @@ export class LensStorage extends SOContentStorage { // 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(CONTENT_ID, dataToLatest, { id, overwrite: true, 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 index 22e1af627297..afbd000805f8 100644 --- 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 @@ -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, }, }, }, 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 new file mode 100644 index 000000000000..e0be3d04393f --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/server/content_management/v1/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 * from './cm_services'; diff --git a/x-pack/platform/plugins/shared/lens/server/index.ts b/x-pack/platform/plugins/shared/lens/server/index.ts index 80a61445b9cd..d3f3eeb83c6c 100644 --- a/x-pack/platform/plugins/shared/lens/server/index.ts +++ b/x-pack/platform/plugins/shared/lens/server/index.ts @@ -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'; diff --git a/x-pack/platform/plugins/shared/lens/server/plugin.tsx b/x-pack/platform/plugins/shared/lens/server/plugin.tsx index ab683555d70c..b3c41328fc85 100644 --- a/x-pack/platform/plugins/shared/lens/server/plugin.tsx +++ b/x-pack/platform/plugins/shared/lens/server/plugin.tsx @@ -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 { private customVisualizationMigrations: CustomVisualizationMigrations = {}; + private readonly logger: Logger; - constructor(private initializerContext: PluginInitializerContext) {} + constructor(private initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } setup(core: CoreSetup, 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: ( diff --git a/x-pack/platform/plugins/shared/lens/tsconfig.json b/x-pack/platform/plugins/shared/lens/tsconfig.json index cac53597bd08..e1380dfd4cd6 100644 --- a/x-pack/platform/plugins/shared/lens/tsconfig.json +++ b/x-pack/platform/plugins/shared/lens/tsconfig.json @@ -124,6 +124,7 @@ "@kbn/fields-metadata-plugin", "@kbn/alerts-ui-shared", "@kbn/deeplinks-analytics", + "@kbn/core-http-server", ], "exclude": ["target/**/*"] } diff --git a/x-pack/platform/test/api_integration/apis/lens/config.ts b/x-pack/platform/test/api_integration/apis/lens/config.ts new file mode 100644 index 000000000000..5f335f116fef --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/config.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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts')); + + return { + ...baseIntegrationTestsConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/platform/test/api_integration/apis/lens/examples.ts b/x-pack/platform/test/api_integration/apis/lens/examples.ts new file mode 100644 index 000000000000..a4da3167673f --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/examples.ts @@ -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', + }, + ], + }, +}); diff --git a/x-pack/platform/test/api_integration/apis/lens/index.ts b/x-pack/platform/test/api_integration/apis/lens/index.ts new file mode 100644 index 000000000000..42fd587c9884 --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/index.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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('lens', () => { + loadTestFile(require.resolve('./visualizations')); + }); +} diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/create/index.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/create/index.ts new file mode 100644 index 000000000000..929a6bff0499 --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/create/index.ts @@ -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')); + }); +} 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 new file mode 100644 index 000000000000..e390eb2f51bb --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/create/main.ts @@ -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); + }); + }); +} 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 new file mode 100644 index 000000000000..07992cbd7481 --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/create/validation.ts @@ -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]' + ); + }); + }); +} diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/delete/index.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/delete/index.ts new file mode 100644 index 000000000000..008870b281c1 --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/delete/index.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 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')); + }); +} 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 new file mode 100644 index 000000000000..b954b0e31236 --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/delete/main.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 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.' + ); + }); + }); +} diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/get/index.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/get/index.ts new file mode 100644 index 000000000000..008870b281c1 --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/get/index.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 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')); + }); +} 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 new file mode 100644 index 000000000000..f30684a456c8 --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/get/main.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 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.' + ); + }); + }); +} diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/index.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/index.ts new file mode 100644 index 000000000000..47f4688d74bd --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/index.ts @@ -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')); + }); +} diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/search/index.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/search/index.ts new file mode 100644 index 000000000000..0e449474750f --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/search/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +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')); + }); +} 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 new file mode 100644 index 000000000000..f0a86f93ce85 --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/search/main.ts @@ -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); + }); + }); +} 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 new file mode 100644 index 000000000000..3cc8eeff85a0 --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/search/validation.ts @@ -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' + ); + }); + }); +} diff --git a/x-pack/platform/test/api_integration/apis/lens/visualizations/update/index.ts b/x-pack/platform/test/api_integration/apis/lens/visualizations/update/index.ts new file mode 100644 index 000000000000..0e449474750f --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/update/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +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')); + }); +} 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 new file mode 100644 index 000000000000..c5e38106a6ab --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/update/main.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 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.' + ); + }); + }); +} 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 new file mode 100644 index 000000000000..777cc9cc82c2 --- /dev/null +++ b/x-pack/platform/test/api_integration/apis/lens/visualizations/update/validation.ts @@ -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]' + ); + }); + }); +} diff --git a/x-pack/platform/test/tsconfig.json b/x-pack/platform/test/tsconfig.json index 88115823cffb..6931b88220fa 100644 --- a/x-pack/platform/test/tsconfig.json +++ b/x-pack/platform/test/tsconfig.json @@ -105,5 +105,6 @@ "@kbn/repo-info", "@kbn/dev-cli-errors", "@kbn/journeys", + "@kbn/lens-plugin", ] } diff --git a/x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json b/x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json new file mode 100644 index 000000000000..1adcdea2ecab --- /dev/null +++ b/x-pack/test/api_integration/fixtures/kbn_archiver/lens/example_docs.json @@ -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" +} \ No newline at end of file